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
.tuing
.tui
/*is aliced*/;
28 import iv
.tuing
.types
;
30 import iv
.tuing
.events
;
31 import iv
.tuing
.controls
.window
;
34 // ////////////////////////////////////////////////////////////////////////// //
35 __gshared
ushort fuiDoubleTime
= 250; // 250 msecs to register doubleclick
38 public void fuiLayout (FuiControl ctl
) { if (ctl
!is null) flexLayout(ctl
.lp
); }
41 // ////////////////////////////////////////////////////////////////////////// //
43 uint def
; // default color
44 uint sel
; // sel is also focus
45 uint mark
; // marked text
46 uint marksel
; // active marked text
48 uint input
; // input field
49 uint inputmark
; // input field marked text (?)
50 uint inputunchanged
; // unchanged input field
51 uint reverse
; // reversed text
52 uint title
; // window title
53 uint disabled
; // disabled text
60 // ////////////////////////////////////////////////////////////////////////// //
61 enum FuiPaletteNormal
= 0;
62 enum FuiPaletteError
= 1;
64 __gshared FuiPalette
[2] fuiPalette
; // default palette
66 shared static this () {
67 fuiPalette
[FuiPaletteNormal
].def
= XtColorFB
!(ttyRgb2Color(0xd0, 0xd0, 0xd0), ttyRgb2Color(0x4e, 0x4e, 0x4e)); // 252,239
69 fuiPalette
[FuiPaletteNormal
].sel
= XtColorFB
!(ttyRgb2Color(0xda, 0xda, 0xda), ttyRgb2Color(0x00, 0x5f, 0x5f)); // 253,23
70 fuiPalette
[FuiPaletteNormal
].mark
= XtColorFB
!(ttyRgb2Color(0xff, 0xff, 0x00), ttyRgb2Color(0x5f, 0x5f, 0x5f)); // 226,59
71 fuiPalette
[FuiPaletteNormal
].marksel
= XtColorFB
!(ttyRgb2Color(0xff, 0xff, 0x87), ttyRgb2Color(0x00, 0x5f, 0x87)); // 228,24
72 fuiPalette
[FuiPaletteNormal
].gauge
= XtColorFB
!(ttyRgb2Color(0xbc, 0xbc, 0xbc), ttyRgb2Color(0x5f, 0x87, 0x87)); // 250,66
73 fuiPalette
[FuiPaletteNormal
].input
= XtColorFB
!(ttyRgb2Color(0xd7, 0xd7, 0xaf), ttyRgb2Color(0x26, 0x26, 0x26)); // 187,235
74 fuiPalette
[FuiPaletteNormal
].inputmark
= XtColorFB
!(ttyRgb2Color(0xff, 0xff, 0x87), ttyRgb2Color(0x00, 0x5f, 0x5f)); // 228,23
75 //fuiPalette[FuiPaletteNormal].inputunchanged = XtColorFB!(ttyRgb2Color(0xff, 0xff, 0xff), ttyRgb2Color(0x26, 0x26, 0x26)); // 144,235
76 fuiPalette
[FuiPaletteNormal
].inputunchanged
= XtColorFB
!(ttyRgb2Color(0xff, 0xff, 0xff), ttyRgb2Color(0x00, 0x00, 0x40));
77 fuiPalette
[FuiPaletteNormal
].reverse
= XtColorFB
!(ttyRgb2Color(0xe4, 0xe4, 0xe4), ttyRgb2Color(0x5f, 0x87, 0x87)); // 254,66
78 fuiPalette
[FuiPaletteNormal
].title
= XtColorFB
!(ttyRgb2Color(0xd7, 0xaf, 0x87), ttyRgb2Color(0x4e, 0x4e, 0x4e)); // 180,239
79 fuiPalette
[FuiPaletteNormal
].disabled
= XtColorFB
!(ttyRgb2Color(0x94, 0x94, 0x94), ttyRgb2Color(0x4e, 0x4e, 0x4e)); // 246,239
81 fuiPalette
[FuiPaletteNormal
].hot
= XtColorFB
!(ttyRgb2Color(0xff, 0xaf, 0x00), ttyRgb2Color(0x4e, 0x4e, 0x4e)); // 214,239
82 fuiPalette
[FuiPaletteNormal
].hotsel
= XtColorFB
!(ttyRgb2Color(0xff, 0xaf, 0x00), ttyRgb2Color(0x00, 0x5f, 0x5f)); // 214,23
84 fuiPalette
[FuiPaletteError
] = fuiPalette
[FuiPaletteNormal
];
85 fuiPalette
[FuiPaletteError
].def
= XtColorFB
!(ttyRgb2Color(0xff, 0xff, 0xd7), ttyRgb2Color(0x5f, 0x00, 0x00)); // 230,52
86 fuiPalette
[FuiPaletteError
].sel
= XtColorFB
!(ttyRgb2Color(0xe4, 0xe4, 0xe4), ttyRgb2Color(0x00, 0x5f, 0x5f)); // 254,23
87 fuiPalette
[FuiPaletteError
].hot
= XtColorFB
!(ttyRgb2Color(0xff, 0x5f, 0x5f), ttyRgb2Color(0x5f, 0x00, 0x00)); // 203,52
88 fuiPalette
[FuiPaletteError
].hotsel
= XtColorFB
!(ttyRgb2Color(0xff, 0x5f, 0x5f), ttyRgb2Color(0x00, 0x5f, 0x5f)); // 203,23
89 fuiPalette
[FuiPaletteError
].title
= XtColorFB
!(ttyRgb2Color(0xff, 0xff, 0x5f), ttyRgb2Color(0x5f, 0x00, 0x00)); // 227,52
91 if (termType
== TermType
.linux
) {
92 fuiPalette
[FuiPaletteNormal
].def
= XtColorFB
!(ttyRgb2Color(0xd0, 0xd0, 0xd0), ttyRgb2Color(0x18, 0x18, 0xb2));
93 fuiPalette
[FuiPaletteNormal
].title
= XtColorFB
!(ttyRgb2Color(0xd7, 0xaf, 0x87), ttyRgb2Color(0x18, 0x18, 0xb2));
94 fuiPalette
[FuiPaletteNormal
].disabled
= XtColorFB
!(ttyRgb2Color(0x94, 0x94, 0x94), ttyRgb2Color(0x18, 0x18, 0xb2));
95 fuiPalette
[FuiPaletteNormal
].hot
= XtColorFB
!(ttyRgb2Color(0xff, 0xaf, 0x00), ttyRgb2Color(0x18, 0x18, 0xb2));
100 // ////////////////////////////////////////////////////////////////////////// //
101 public class FuiCtlLayoutProps
: FuiLayoutProps
{
103 this (FuiControl actl
) { ctl
= actl
; }
104 override void layoutingStarted () { if (ctl
!is null) ctl
.layoutingStarted(); }
105 override void layoutingComplete () { if (ctl
!is null) ctl
.layoutingComplete(); }
109 // ////////////////////////////////////////////////////////////////////////// //
110 public class FuiControl
: EventTarget
{
114 CanBeFocused
= 1U<<0, // this item can be focused
115 Disabled
= 1U<<1, // this item is dimed
116 Hovered
= 1U<<2, // this item is hovered
117 Active
= 1U<<3, // mouse is pressed on this
118 Focused
= 1U<<4, // mouse is pressed on this
121 FuiPalette pal
; // custom palette
122 int palidx
= FuiPaletteNormal
;
123 FuiCtlLayoutProps lp
;
125 string id
; // not used by the engine itself
126 string caption
; // use "&k" to mark hotkey
127 bool defctl
; // set to `true` to make this control respond to "default"
128 bool escctl
; // set to `true` to make this control respond to "cancel"
129 bool hotkeyed
; // set to `true` to make `tryHotKey()` check for hotkey in caption
130 protected string
[2] groupid
;
132 void delegate (FuiControl self
) onAction
;
133 void delegate (FuiControl self
, XtWindow win
) onDraw
;
134 void delegate (FuiControl self
) onBlur
;
136 protected void layoutingStarted () {
138 //{ import iv.vfs.io; VFile("zlx.log", "a").writefln("me: %s; parent: 0x%08x", this.classinfo.name, *cast(void**)&lp.parent); }
139 if (lp
.parent
is null) {
140 FuiControl
[][string
][2] grp
;
141 forEach((FuiControl ctl
) {
142 ctl
.lp
.groupNext
[] = null;
143 foreach (immutable idx
; 0..2) {
144 if (ctl
.groupid
[idx
].length
) {
145 if (auto ap
= ctl
.groupid
[idx
] in grp
[idx
]) (*ap
) ~= ctl
; else grp
[idx
][ctl
.groupid
[idx
]] = [ctl
];
150 foreach (immutable gidx
; 0..2) {
151 foreach (FuiControl
[] carr
; grp
[gidx
].byValue
) {
152 foreach (immutable xidx
, FuiControl c
; carr
) {
153 if (xidx
> 0) carr
[xidx
-1].lp
.groupNext
[gidx
] = c
.lp
;
160 protected void layoutingComplete () {}
162 this (FuiControl aparent
) {
163 lp
= new FuiCtlLayoutProps(this);
165 if (aparent
!is null) {
166 lp
.parent
= aparent
.lp
;
167 auto lcc
= aparent
.lp
.firstChild
;
170 aparent
.lp
.firstChild
= lp
;
173 while (lcc
.nextSibling
!is null) lcc
= lcc
.nextSibling
;
174 lcc
.nextSibling
= lp
;
177 this.connectListeners();
180 alias Orientation
= FuiLayoutProps
.Orientation
;
181 alias Align
= FuiLayoutProps
.Align
;
183 final @property pure nothrow @safe @nogc {
184 // this may return null if you screwed the things
185 inout(FuiDeskWindow
) topwindow () inout @trusted { if (auto plp
= cast(inout(FuiCtlLayoutProps
))lp
.parent
) return cast(typeof(return))plp
.ctl
.toplevel
; else return null; }
187 inout(FuiControl
) parent () inout { if (auto plp
= cast(inout(FuiCtlLayoutProps
))lp
.parent
) return plp
.ctl
; else return null; }
188 inout(FuiControl
) toplevel () inout { if (auto plp
= cast(inout(FuiCtlLayoutProps
))lp
.parent
) return plp
.ctl
.toplevel
; else return this; }
189 inout(FuiControl
) nextSibling () inout { if (auto nlp
= cast(inout(FuiCtlLayoutProps
))lp
.nextSibling
) return nlp
.ctl
; else return null; }
190 inout(FuiControl
) firstChild () inout { if (auto flp
= cast(inout(FuiCtlLayoutProps
))lp
.firstChild
) return flp
.ctl
; else return null; }
192 inout(FuiControl
) lastChild () inout @trusted {
193 if (auto fcc
= cast(FuiControl
)firstChild
) {
195 if (auto nc
= fcc
.nextSibling
) fcc
= nc
; else break;
197 return cast(typeof(return))fcc
;
202 alias previousSibling
= prevSibling
; // insanely long name
203 inout(FuiControl
) prevSibling () inout @trusted {
204 FuiControl prev
= null;
205 if (auto plp
= cast(inout(FuiCtlLayoutProps
))lp
.parent
) {
206 for (FuiControl cc
= cast(FuiControl
)plp
.firstChild
; cc
!is null; prev
= cc
, cc
= cc
.nextSibling
) {
207 if (cc
is this) break;
210 return cast(typeof(return))prev
;
214 void closetop(bool withme
=true) () {
215 if (auto w
= topwindow
) {
217 (new FuiEventClose(w
, this)).post
;
219 (new FuiEventClose(w
, null)).post
;
224 // ////////////////////////////////////////////////////////////////////////// //
225 final uint palColor(string name
) () const nothrow @trusted @nogc {
226 static if (is(typeof(mixin("FuiPalette."~name
)))) {
227 for (auto ctl
= cast(FuiControl
)this; ctl
!is null; ctl
= ctl
.parent
) {
228 if (auto res
= mixin("ctl.pal."~name
)) return res
;
230 if (palidx
>= 0 && palidx
< fuiPalette
.length
) {
231 if (auto res
= mixin("fuiPalette[palidx]."~name
)) return res
;
234 return XtColorFB
!(7, 0);
238 // EventTarget interface
240 // this should return parent object or null
241 @property Object
eventbusParent () { return parent
; }
242 // this will be called on sinking and bubbling
243 void eventbusOnEvent (Event evt
) {}
247 final @property pure nothrow @safe @nogc {
248 bool hovered () const { pragma(inline
, true); return ((flags
&Flags
.Hovered
) != 0); }
249 bool active () const { pragma(inline
, true); return ((flags
&Flags
.Active
) != 0); }
250 bool focused () const { pragma(inline
, true); return ((flags
&Flags
.Focused
) != 0); }
252 bool canBeFocused () const { return (lp
.visible
&& (flags
&(Flags
.CanBeFocused|Flags
.Disabled
)) == Flags
.CanBeFocused
); }
253 void canBeFocused (bool v
) { pragma(inline
, true); if (v
) flags |
= Flags
.CanBeFocused
; else flags
&= ~Flags
.CanBeFocused
; }
255 bool enabled () const { pragma(inline
, true); return ((flags
&Flags
.Disabled
) == 0); }
256 void enabled (bool v
) { pragma(inline
, true); if (v
) flags
&= ~Flags
.Disabled
; else flags
= (flags|Flags
.Disabled
)&~(Flags
.Hovered|Flags
.Active
); }
258 bool disabled () const { pragma(inline
, true); return ((flags
&Flags
.Disabled
) != 0); }
259 void disabled (bool v
) { pragma(inline
, true); if (v
) flags
= (flags|Flags
.Disabled
)&~(Flags
.Hovered|Flags
.Active
); else flags
&= ~Flags
.Disabled
; }
261 bool visible () const { pragma(inline
, true); return lp
.visible
; }
262 void visible (bool v
) { pragma(inline
, true); lp
.visible
= v
; }
264 bool hidden () const { pragma(inline
, true); return !lp
.visible
; }
265 void hidden (bool v
) { pragma(inline
, true); lp
.visible
= !v
; }
267 bool lineBreak () const { pragma(inline
, true); return lp
.lineBreak
; }
268 void lineBreak (bool v
) { pragma(inline
, true); lp
.lineBreak
= v
; }
270 bool ignoreSpacing () const { pragma(inline
, true); return lp
.ignoreSpacing
; }
271 void ignoreSpacing (bool v
) { pragma(inline
, true); lp
.ignoreSpacing
= v
; }
273 bool horizontal () const { pragma(inline
, true); return (lp
.orientation
== lp
.Orientation
.Horizontal
); }
274 bool vertical () const { pragma(inline
, true); return (lp
.orientation
== lp
.Orientation
.Vertical
); }
276 void horizontal (bool v
) { pragma(inline
, true); lp
.orientation
= (v ? lp
.Orientation
.Horizontal
: lp
.Orientation
.Vertical
); }
277 void vertical (bool v
) { pragma(inline
, true); lp
.orientation
= (v ? lp
.Orientation
.Vertical
: lp
.Orientation
.Horizontal
); }
279 Align
aligning () const { pragma(inline
, true); return lp
.aligning
; }
280 void aligning (Align v
) { pragma(inline
, true); lp
.aligning
= v
; }
282 int flex () const { pragma(inline
, true); return lp
.flex
; }
283 void flex (int v
) { pragma(inline
, true); lp
.flex
= v
; }
285 int spacing () const { pragma(inline
, true); return lp
.spacing
; }
286 void spacing (int v
) { pragma(inline
, true); lp
.spacing
= v
; }
288 int lineSpacing () const { pragma(inline
, true); return lp
.lineSpacing
; }
289 void lineSpacing (int v
) { pragma(inline
, true); lp
.lineSpacing
= v
; }
291 ref inout(FuiMargin
) padding () inout { pragma(inline
, true); return lp
.padding
; }
292 void padding (FuiMargin v
) { pragma(inline
, true); lp
.padding
= v
; }
294 ref inout(FuiSize
) minSize () inout { pragma(inline
, true); return lp
.minSize
; }
295 void minSize (FuiSize v
) { pragma(inline
, true); lp
.minSize
= v
; }
297 ref inout(FuiSize
) maxSize () inout { pragma(inline
, true); return lp
.maxSize
; }
298 void maxSize (FuiSize v
) { pragma(inline
, true); lp
.maxSize
= v
; }
300 // calculated item dimensions
301 ref inout(FuiPoint
) pos () inout { pragma(inline
, true); return lp
.pos
; }
302 void pos (FuiPoint v
) { pragma(inline
, true); lp
.pos
= v
; }
304 ref inout(FuiSize
) size () inout { pragma(inline
, true); return lp
.size
; }
305 void size (FuiSize v
) { pragma(inline
, true); lp
.size
= v
; }
307 ref inout(FuiRect
) rect () inout { pragma(inline
, true); return lp
.rect
; }
308 void rect (FuiRect v
) { pragma(inline
, true); lp
.rect
= v
; }
310 FuiPoint
toGlobal (FuiPoint pt
) const { return lp
.toGlobal(pt
); }
313 void hovered (bool v
) { pragma(inline
, true); if (v
) flags |
= Flags
.Hovered
; else flags
&= ~Flags
.Hovered
; }
314 void active (bool v
) { pragma(inline
, true); if (v
) flags |
= Flags
.Active
; else flags
&= ~Flags
.Active
; }
315 void focused (bool v
) { pragma(inline
, true); if (v
) flags |
= Flags
.Focused
; else flags
&= ~Flags
.Focused
; }
319 protected ubyte clickMask
; // buttons that can be used to click this item to do some action
320 protected ubyte doubleMask
; // buttons that can be used to double-click this item to do some action
322 final bool canAcceptClick (TtyEvent
.MButton
bt) {
324 (bt >= TtyEvent
.MButton
.First
&& bt-TtyEvent
.MButton
.First
< 8 ?
325 ((clickMask
&(1<<bt-TtyEvent
.MButton
.First
)) != 0) : false);
328 final void acceptClick (TtyEvent
.MButton
bt, bool v
=true) {
329 if (bt >= TtyEvent
.MButton
.First
&& bt-TtyEvent
.MButton
.First
< 8) {
331 clickMask |
= cast(ubyte)(1<<(bt-TtyEvent
.MButton
.First
));
333 clickMask
&= cast(ubyte)~(1<<(bt-TtyEvent
.MButton
.First
));
338 final bool canAcceptDouble (TtyEvent
.MButton
bt) {
340 (bt >= TtyEvent
.MButton
.First
&& bt-TtyEvent
.MButton
.First
< 8 ?
341 ((doubleMask
&(1<<(bt-TtyEvent
.MButton
.First
))) != 0) : false);
344 final void acceptDouble (TtyEvent
.MButton
bt, bool v
=true) {
345 if (bt >= TtyEvent
.MButton
.First
&& bt-TtyEvent
.MButton
.First
< 8) {
347 doubleMask |
= cast(ubyte)(1<<(bt-TtyEvent
.MButton
.First
));
349 doubleMask
&= cast(ubyte)~(1<<(bt-TtyEvent
.MButton
.First
));
354 // depth first; calls delegate for itself too
355 final FuiControl
forEach() (scope bool delegate (FuiControl ctl
) dg
) {
356 if (dg
is null) return null;
357 FuiControl
descend() (FuiControl c
) {
359 if (auto cx
= descend(c
.firstChild
)) return cx
;
365 if (dg(this)) return this;
366 return descend(this.firstChild
);
369 final FuiEventQueueDesk
getDesk () {
370 if (auto win
= cast(FuiDeskWindow
)toplevel
) return win
.desk
;
374 final FuiControl
opIndex (const(char)[] id
) {
375 if (id
.length
== 0) return null;
376 return forEach((FuiControl ctl
) => (ctl
.id
== id
));
379 final @property void hgroup (string v
) { groupid
[lp
.Orientation
.Horizontal
] = v
; }
380 final @property void vgroup (string v
) { groupid
[lp
.Orientation
.Vertical
] = v
; }
381 final @property string
hgroup () { return groupid
[lp
.Orientation
.Horizontal
]; }
382 final @property string
vgroup () { return groupid
[lp
.Orientation
.Vertical
]; }
385 if (onAction
!is null) onAction(this);
388 bool tryHotKey (TtyEvent key
) {
389 if (!hotkeyed
) return false;
390 auto hotch
= XtWindow
.hotChar(caption
).tolower
;
391 if (hotch
== 0) return false;
392 if (key
.key
== TtyEvent
.Key
.ModChar
&& !key
.ctrl
&& key
.alt
&& key
.ch
< 128 && tolower(cast(char)key
.ch
) == hotch
) return true;
393 if (key
.key
== TtyEvent
.Key
.Char
&& key
.ch
< 128 && tolower(cast(char)key
.ch
) == hotch
) return true;
397 protected void drawChildren (XtWindow win
) {
398 if (lp
.firstChild
is null) return;
400 auto mgb
= lp
.toGlobal(FuiPoint(0, 0));
401 auto osc
= ttyScissor
;
402 scope(exit
) ttyScissor
= osc
;
403 ttyScissor
= ttyScissor
.crop(mgb
.x
, mgb
.y
, lp
.size
.w
, lp
.size
.h
);
404 if (!ttyScissor
.visible
) return;
405 for (auto cc
= firstChild
; cc
!is null; cc
= cc
.nextSibling
) {
406 if (!cc
.visible
) continue;
407 mgb
= cc
.lp
.toGlobal(FuiPoint(0, 0));
408 cc
.draw(XtWindow(mgb
.x
, mgb
.y
, cc
.lp
.size
.w
, cc
.lp
.size
.h
));
412 // this one is without scissors; used to draw shadows
413 protected void drawSelfPre (XtWindow win
) {
416 protected void drawSelfPost (XtWindow win
) {
419 protected void drawSelf (XtWindow win
) {
420 if (onDraw
is null) {
421 win
.color
= palColor
!"def"();
422 win
.fill(0, 0, win
.width
, win
.height
);
428 public void draw (XtWindow win
) {
429 if (!lp
.visible
) return;
430 if (lp
.size
.w
< 1 || lp
.size
.h
< 1) return;
432 auto mgb
= lp
.toGlobal(FuiPoint(0, 0));
433 drawSelfPre(XtWindow(mgb
.x
, mgb
.y
, lp
.size
.w
, lp
.size
.h
));
434 auto osc
= ttyScissor
;
435 scope(exit
) ttyScissor
= osc
;
436 ttyScissor
= ttyScissor
.crop(mgb
.x
, mgb
.y
, lp
.size
.w
, lp
.size
.h
);
437 if (!ttyScissor
.visible
) return;
438 auto csc
= ttyScissor
;
439 drawSelf(XtWindow(mgb
.x
, mgb
.y
, lp
.size
.w
, lp
.size
.h
));
441 drawChildren(XtWindow(mgb
.x
, mgb
.y
, lp
.size
.w
, lp
.size
.h
));
443 drawSelfPost(XtWindow(mgb
.x
, mgb
.y
, lp
.size
.w
, lp
.size
.h
));
446 void onMyEvent (FuiEventFocus evt
) { if (canBeFocused || lp
.parent
is null) focused
= true; }
447 void onMyEvent (FuiEventBlur evt
) { focused
= false; if (onBlur
!is null) onBlur(this); }
448 void onMyEvent (FuiEventActive evt
) { active
= true; }
449 void onMyEvent (FuiEventInactive evt
) { active
= false; }
452 void onMyEvent (FuiEventClick evt) {
453 if (!canBeFocused) return;
454 if (auto desk = getDesk) desk.switchFocusTo(this);
460 // ////////////////////////////////////////////////////////////////////////// //
461 class FuiEventQueue
{
463 Weak
!FuiControl lastHover
, lastFocus
;
464 ubyte lastButtons
, lastMods
;
465 FuiPoint lastMouse
= FuiPoint(-666, -666); // last mouse coordinates
466 int[8] lastClickDelta
= int.max
; // how much time passed since last click with the given button was registered?
467 Weak
!FuiControl
[8] lastClick
; // on which item it was registered?
468 ubyte[8] beventCount
; // oooh...
472 lastHover
= new Weak
!FuiControl();
473 lastFocus
= new Weak
!FuiControl();
474 foreach (ref lcc
; lastClick
) lcc
= new Weak
!FuiControl();
478 abstract FuiControl
atXY (FuiPoint pt
);
480 abstract void switchFocusTo (FuiControl ctl
, bool allowWindowSwitch
=false);
482 void fixHovering () {
483 auto lho
= lastHover
.object
;
484 if (lho
!is null && (lho
.hidden || lho
.disabled
)) {
485 (new FuiEventLeave(lastHover
.object
)).post
;
486 lastHover
.object
= null;
489 auto nh
= atXY(lastMouse
);
490 if (nh
!is null && (nh
.hidden || nh
.disabled
)) nh
= null;
492 if (lho
!is null) (new FuiEventLeave(lastHover
.object
)).post
;
493 lastHover
.object
= nh
;
494 if (nh
!is null) (new FuiEventEnter(nh
)).post
;
498 // return `false` if event wasn't processed
499 bool queue (TtyEvent key
) {
500 if (key
.key
== TtyEvent
.Key
.None
) return false;
501 if (key
.key
== TtyEvent
.Key
.Error
) return false;
502 if (key
.key
== TtyEvent
.Key
.Unknown
) return false;
505 auto pt
= FuiPoint(key
.x
, key
.y
);
509 if (key
.button
!= TtyEvent
.MButton
.None
) {
510 if (key
.mpress || key
.mrelease
) {
511 newButtonState(key
.button
-TtyEvent
.MButton
.First
, key
.mpress
);
512 } else if (key
.mwheel
) {
513 // rawtty workaround: send press and release
514 newButtonState(key
.button
-TtyEvent
.MButton
.First
, true);
515 newButtonState(key
.button
-TtyEvent
.MButton
.First
, false);
520 fixHovering(); // anyway, 'cause toplevel widget can be changed
521 if (auto fcs
= lastFocus
.object
) {
523 //if (key.focusin) { (new FuiEventFocus(fcs)).post; return true; }
524 //if (key.focusout) { (new FuiEventBlur(fcs)).post; return true; }
525 if (key
.focusin || key
.focusout
) return false;
526 (new FuiEventKey(fcs
, key
)).post
;
534 void newButtonState (uint bidx
, bool down
) {
536 // 0: nothing was pressed or released yet
537 // 1: button was pressed for the first time
538 // 2: button was released for the first time
539 // 3: button was pressed for the second time
540 // 4: button was released for the second time
542 // reset "active" control state
543 void resetActive() () {
544 if (auto i
= lastClick
[bidx
].object
) {
545 foreach (immutable idx
, Weak
!FuiControl lc
; lastClick
) {
546 if (idx
!= bidx
&& lc
.object
is i
) return;
548 (new FuiEventInactive(i
)).post
;
552 void doRelease() () {
554 auto lp
= lastHover
.object
;
555 // did we released the button on the same control we pressed it?
556 if (beventCount
[bidx
] == 0 || lp
is null ||
(lp
!is lastClick
[bidx
].object
)) {
557 // no, this is nothing, reset all info
558 lastClick
[bidx
].object
= null;
559 beventCount
[bidx
] = 0;
562 // yep, check which kind of event this is
563 if (beventCount
[bidx
] == 3 && (lp
.doubleMask
&(1<<bidx
)) != 0) {
564 // we accepts doubleclicks, and this can be doubleclick
565 if (lastClickDelta
[bidx
] <= fuiDoubleTime
) {
566 // it comes right in time too
567 if (lp
.enabled
) (new FuiEventDouble(lp
, lp
.lp
.toLocal(lastMouse
), cast(TtyEvent
.MButton
)(TtyEvent
.MButton
.First
+bidx
))).post
;
568 // continue registering doubleclicks
569 lastClickDelta
[bidx
] = 0;
570 beventCount
[bidx
] = 2;
573 // this is invalid doubleclick, revert to simple click
574 beventCount
[bidx
] = 1;
575 // start registering doubleclicks
576 lastClickDelta
[bidx
] = 0;
579 if (beventCount
[bidx
] == 1) {
580 if (lp
.clickMask
&(1<<bidx
)) {
581 if (lp
.enabled
) (new FuiEventClick(lp
, lp
.lp
.toLocal(lastMouse
), cast(TtyEvent
.MButton
)(TtyEvent
.MButton
.First
+bidx
))).post
;
583 // start doubleclick timer
584 beventCount
[bidx
] = ((lp
.doubleMask
&(1<<bidx
)) != 0 ?
2 : 0);
585 // start registering doubleclicks
586 lastClickDelta
[bidx
] = 0;
589 // something unexpected, reset it all
590 lastClick
[bidx
].object
= null;
591 beventCount
[bidx
] = 0;
592 lastClickDelta
[bidx
] = lastClickDelta
[0].max
;
597 auto lp
= lastHover
.object
;
600 lastClick
[bidx
].object
= null;
601 beventCount
[bidx
] = 0;
602 lastClickDelta
[bidx
] = lastClickDelta
[0].max
;
606 if (beventCount
[bidx
] == 0) {
608 lastClick
[bidx
].object
= lp
;
609 beventCount
[bidx
] = 1;
610 lastClickDelta
[bidx
] = lastClickDelta
[0].max
;
612 if (lp
.canBeFocused
) switchFocusTo(lp
);
613 if ((lp
.clickMask
&(1<<bidx
)) != 0) (new FuiEventActive(lp
)).post
;
617 if (beventCount
[bidx
] == 2) {
618 // start double if control is the same
619 if (lastClick
[bidx
].object
is lp
) {
621 if (lastClickDelta
[bidx
] > fuiDoubleTime
) {
622 // reset double to single
623 beventCount
[bidx
] = 1;
624 lastClickDelta
[bidx
] = lastClickDelta
[0].max
;
626 beventCount
[bidx
] = 3;
629 // other, reset to "first press"
630 lastClick
[bidx
].object
= lp
;
631 beventCount
[bidx
] = 1;
632 lastClickDelta
[bidx
] = lastClickDelta
[0].max
;
635 if (lp
.canBeFocused
) switchFocusTo(lp
);
636 if (((lp
.doubleMask|lp
.clickMask
)&(1<<bidx
)) != 0) (new FuiEventActive(lp
)).post
;
640 // something unexpected, reset all
641 lastClick
[bidx
].object
= null;
642 beventCount
[bidx
] = 0;
643 lastClickDelta
[bidx
] = lastClickDelta
[0].max
;
646 if (bidx
>= lastClickDelta
.length
) return;
649 if ((lastButtons
&(1<<bidx
)) != 0) return; // state didn't changed
650 lastButtons |
= cast(ubyte)(1<<bidx
);
654 if ((lastButtons
&(1<<bidx
)) == 0) return; // state didn't changed
655 lastButtons
&= cast(ubyte)~(1<<bidx
);
662 // ////////////////////////////////////////////////////////////////////////// //
663 class FuiEventQueueDesk
: FuiEventQueue
{
664 static struct WinInfo
{
665 enum Type
{ Normal
, Modal
, Popup
}
667 Type type
= Type
.Normal
;
668 @property const pure nothrow @safe @nogc {
669 bool shouldSendWindowBlurToOthers () { return (type
== Type
.Normal
); }
670 bool shouldCloseOnBlur () { return (type
== Type
.Popup
); }
671 bool canBeBlurred () { return (type
== Type
.Normal || type
== Type
.Popup
); }
674 WinInfo
[] winlist
; // latest is on the top
675 FuiDeskWindow
[] wintoplist
; // "on top" windows, latest is on the top
676 void delegate (FuiEventQueueDesk desk
) drawDesk
; // draw desktop background
678 static struct HotKey
{
680 void delegate () handler
;
683 TtyEvent
[] hkcurcombo
;
685 final TtyEvent
[] parseHotCombo (TtyEvent
[] dest
, const(char)[] hkstr
) {
689 while (hkstr
.length
) {
690 hkstr
= TtyEvent
.parse(key
, hkstr
);
691 if (key
.key
== TtyEvent
.Key
.Error
) throw new Exception("invalid hotkey '"~ostr
.idup
~"'");
692 if (key
.key
== TtyEvent
.Key
.None
) break;
693 if (cp
>= dest
.length
) throw new Exception("hotkey combo too long: '"~ostr
.idup
~"'");
694 dest
.ptr
[cp
++] = key
;
699 final bool hasHotKey (const(char)[] hkstr
) {
702 auto cb
= parseHotCombo(cbuf
[], hkstr
);
703 foreach (const ref hk
; hkcombos
) if (hk
.combo
== cb
) return true;
704 } catch (Exception
) {
709 final bool removeHotKey (const(char)[] hkstr
) {
712 auto cb
= parseHotCombo(cbuf
[], hkstr
);
713 foreach (immutable idx
, ref hk
; hkcombos
) {
714 if (hk
.combo
== cb
) {
715 foreach (immutable c
; idx
+1..hkcombos
.length
) hkcombos
[c
-1] = hkcombos
[c
];
716 hkcombos
[$-1] = HotKey
.init
;
717 hkcombos
.length
-= 1;
718 hkcombos
.assumeSafeAppend
;
722 } catch (Exception
) {
727 // return `true` if hotkey was overriden
728 final bool registerHotKey(bool allowOverride
=true) (const(char)[] hkstr
, void delegate () hh
) {
729 if (hh
is null) throw new Exception("empty handler for hotkey");
731 auto cb
= parseHotCombo(cbuf
[], hkstr
);
732 foreach (ref hk
; hkcombos
) {
733 if (hk
.combo
== cb
) {
734 static if (allowOverride
) hk
.handler
= hh
;
738 hkcombos
~= HotKey(cb
.dup
, hh
);
742 final WinInfo
* findWinInfo (FuiDeskWindow w
) {
743 foreach_reverse (ref WinInfo wi
; winlist
) if (wi
.win
is w
) return &wi
;
747 final bool isOnTopWindow (FuiControl ctl
) {
748 if (ctl
is null) return false;
750 foreach_reverse (FuiControl w
; wintoplist
) if (w
is ctl
) return true;
754 final bool isNormalWindow (FuiControl ctl
) {
755 if (ctl
is null) return false;
757 foreach_reverse (ref WinInfo w
; winlist
) if (w
.win
is ctl
) return true;
761 // normal top-level window
762 final bool isTopWindow (FuiControl ctl
) {
763 if (ctl
is null || winlist
.length
== 0) return false;
764 return (ctl
.toplevel
is winlist
[$-1].win
);
768 override FuiControl
atXY (FuiPoint pt
) {
769 static FuiControl
descend (FuiControl ctl
, FuiPoint pt
) {
770 FuiControl lasthit
= null;
771 if (ctl
!is null && ctl
.lp
.visible
) {
773 if (pt
.x
>= 0 && pt
.y
>= 0 && pt
.x
< ctl
.lp
.size
.w
&& pt
.y
< ctl
.lp
.size
.h
) {
775 for (auto cx
= ctl
.firstChild
; cx
!is null; cx
= cx
.nextSibling
) {
776 if (!cx
.visible
) continue;
777 auto ht
= descend(cx
, pt
);
778 if (ht
!is null) lasthit
= ht
;
784 auto lp
= lastFocus
.object
;
785 if (lp
!is null) return descend(lp
.toplevel
, pt
);
786 // check ontop windows
787 foreach_reverse (FuiDeskWindow tw
; wintoplist
) {
788 if (auto cc
= descend(tw
, pt
)) return cc
;
790 // check normal top-level window
791 if (winlist
.length
) return descend(winlist
[$-1].win
, pt
);
795 // pwi.win can be null, but should not be current top-level
796 // pwi is "previous active window"
797 // if window is closed, it should be removed from winlist before calling this
798 // you may (and probably should) pass removed window as pwi
799 private void topwindowFocusJustChanged (WinInfo pwi
=WinInfo
.init
) {
800 if (winlist
.length
&& pwi
.win
is winlist
[$-1].win
) return; // just in case
801 // send blur event to pwi.win
802 if (pwi
.win
!is null) {
803 // does currest focused control belongs to pwi?
804 auto fcs
= lastFocus
.object
;
805 if (fcs
!is null && fcs
.toplevel
is pwi
.win
) {
806 // yes, send blur event to it
807 lastFocus
.object
= null;
808 if (fcs
!is pwi
.win
) (new FuiEventBlur(fcs
)).post
;
809 if (winlist
.length
== 0 || winlist
[$-1].shouldSendWindowBlurToOthers
) {
810 if (pwi
.shouldCloseOnBlur
) (new FuiEventClose(pwi
.win
, null)).post
; else (new FuiEventBlur(pwi
.win
)).post
;
814 // just in case: another blur attempt
815 if (auto fcs
= lastFocus
.object
) {
816 if (fcs
!is null && (winlist
.length
== 0 || fcs
.toplevel
!is winlist
[$-1].win
)) {
817 lastFocus
.object
= null;
818 if (fcs
!is fcs
.toplevel
) (new FuiEventBlur(fcs
)).post
;
819 if (winlist
.length
== 0 || winlist
[$-1].shouldSendWindowBlurToOthers
) {
820 auto tl
= fcs
.toplevel
;
821 foreach (ref WinInfo wi
; winlist
) {
823 if (wi
.shouldCloseOnBlur
) (new FuiEventClose(wi
.win
, null)).post
; else (new FuiEventBlur(wi
.win
)).post
;
830 // old active window is blurred, focus new one
831 if (winlist
.length
== 0) return;
832 auto w
= winlist
[$-1].win
;
834 (new FuiEventFocus(w
)).post
;
835 if (w
.lastfct
is null) w
.lastfct
= w
.findFirstToFocus
;
836 lastFocus
.object
= (w
.lastfct
is null ? w
: w
.lastfct
);
837 if (auto fcs
= lastFocus
.object
) (new FuiEventFocus(fcs
)).post
;
840 // can switch focus from window to window
841 override void switchFocusTo (FuiControl ctl
, bool allowWindowSwitch
=false) {
842 if (ctl
is null) return;
843 if (!ctl
.canBeFocused
) return;
844 auto win
= ctl
.topwindow
;
845 if (win
is null) return; // top-level object is not a window, get out of here
846 if (win
.desk
!is this) return; // not our window, get out too
848 auto ofc
= lastFocus
.object
;
849 if (ofc
is ctl
) return;
850 // if we are trying to focus the window itself, try to find a child to focus
851 FuiControl realfct
= ctl
;
853 realfct
= win
.lastfct
;
854 if (realfct
is null) {
855 realfct
= win
.findFirstToFocus();
856 if (realfct
is null) realfct
= ctl
;
859 // should we bring ctl window on top?
860 if (!isTopWindow(win
) && isNormalWindow(win
)) {
861 if (!allowWindowSwitch
) return; // disabled
862 if (winlist
.length
< 2) { ttyBeep
; return; } // error!
863 if (!winlist
[$-1].canBeBlurred
) return; // current window can't be blurred
864 // move new focused window on top
865 foreach_reverse (immutable idx
, ref WinInfo wi
; winlist
[0..$-1]) {
868 foreach (immutable c
; idx
+1..winlist
.length
) winlist
[c
-1] = winlist
[c
];
870 if (realfct
.parent
!is null) winlist
[$-1].win
.lastfct
= realfct
;
871 topwindowFocusJustChanged(winlist
[$-2]);
878 // remove focus from current focused ctl
880 lastFocus
.object
= null;
881 // don't send blur if current focused object is window itself
882 if (ofc
.parent
!is null) (new FuiEventBlur(ofc
)).post
;
886 lastFocus
.object
= realfct
;
887 if (realfct
!is win
) {
888 win
.lastfct
= realfct
;
889 (new FuiEventFocus(ctl
)).post
;
894 this.connectListeners();
898 protected void addWindowWithType (FuiDeskWindow w
, WinInfo
.Type type
) {
899 if (w
is null) return;
900 if (w
.desk
!is null) return;
902 winlist
~= WinInfo(w
, type
);
903 topwindowFocusJustChanged();
904 assert(lastFocus
.object
!is null);
907 void addWindow (FuiDeskWindow w
) { addWindowWithType(w
, WinInfo
.Type
.Normal
); }
908 void addModal (FuiDeskWindow w
) { addWindowWithType(w
, WinInfo
.Type
.Modal
); }
909 void addPopup (FuiDeskWindow w
) { addWindowWithType(w
, WinInfo
.Type
.Popup
); }
911 override bool queue (TtyEvent key
) {
913 if (hkcombos
.length
) {
916 foreach (ref hk
; hkcombos
) {
917 if (hk
.combo
== hkcurcombo
) {
918 hkcurcombo
.length
= 0;
919 hkcurcombo
.assumeSafeAppend
;
922 } else if (!wasHit
&& hk
.combo
.length
> hkcurcombo
.length
&& hk
.combo
[0..hkcurcombo
.length
] == hkcurcombo
) {
926 if (wasHit
) return true; // combo in progress
927 // no combo in progress; exit if we have some previous combo keys
928 auto doexit
= (hkcurcombo
.length
> 1);
929 hkcurcombo
.length
= 0;
930 hkcurcombo
.assumeSafeAppend
;
931 if (doexit
) return true;
932 // no previous combo keys, continue
934 // check if we clicked on another window and activate it
935 // but only if current top window can be blurred (i.e. deactivated) this way
936 if (key
.mpress
&& winlist
.length
&& winlist
[$-1].canBeBlurred
) {
937 auto pt
= FuiPoint(key
.x
, key
.y
);
938 // if top window is popup, and user clicked outside of it, close it
939 if (winlist
.length
&& winlist
[$-1].type
== WinInfo
.Type
.Popup
&& !pt
.inside(winlist
[$-1].win
.rect
)) {
940 (new FuiEventClose(winlist
[$-1].win
, null)).post
;
941 } else if (winlist
.length
&& winlist
[$-1].type
== WinInfo
.Type
.Modal
&& !pt
.inside(winlist
[$-1].win
.rect
)) {
942 // do nothing, as modal window cannot be dismissed this way
943 } else if (winlist
.length
> 1 && !pt
.inside(winlist
[$-1].win
.rect
)) {
944 foreach_reverse (immutable idx
, ref WinInfo wi
; winlist
[0..$-1]) {
946 if (w
.hidden || w
.disabled
) continue;
947 if (pt
.inside(w
.lp
.rect
)) {
948 auto lastWF
= winlist
[$-1];
950 foreach (immutable c
; idx
+1..winlist
.length
) winlist
[c
-1] = winlist
[c
];
952 topwindowFocusJustChanged(lastWF
);
958 return super.queue(key
);
962 if (drawDesk
is null) {
963 XtWindow win
= XtWindow
.fullscreen
;
964 //win.color = XtColorFB!(7, 0);
965 win
.color
= XtColorFB
!(TtyRgb2Color
!(0x00, 0x00, 0x00), TtyRgb2Color
!(0x00, 0x5f, 0xaf));
966 win
.fill
!true(0, 0, win
.width
, win
.height
, 'a');
970 foreach (ref WinInfo wi
; winlist
) wi
.win
.draw(XtWindow(wi
.win
.lp
.pos
.x
, wi
.win
.lp
.pos
.y
, wi
.win
.lp
.size
.w
, wi
.win
.lp
.size
.h
));
971 foreach (FuiDeskWindow w
; wintoplist
) w
.draw(XtWindow(w
.lp
.pos
.x
, w
.lp
.pos
.y
, w
.lp
.size
.w
, w
.lp
.size
.h
));
974 void onEvent (FuiEventClose evt
) {
975 if (evt
.source
is null) return;
976 if (auto ww
= cast(FuiDeskWindow
)evt
.source
) {
977 if (ww
.desk
!is this) return;
981 // reverse usually faster
982 foreach_reverse (immutable idx
, ref WinInfo twi
; winlist
) {
983 FuiDeskWindow tw
= twi
.win
;
984 if (evt
.source
is tw
) {
986 auto lastWF
= (idx
== winlist
.length
-1 ? twi
: WinInfo
.init
);
987 foreach (immutable c
; idx
+1..winlist
.length
) winlist
[c
-1] = winlist
[c
];
988 winlist
[$-1] = WinInfo
.init
;
990 winlist
.assumeSafeAppend
;
991 if (lastWF
.win
!is null) { topwindowFocusJustChanged(lastWF
); lastWF
.win
.desk
= null; }
993 if (winlist
.length
== 0 && wintoplist
.length
== 0) (new FuiEventQuit
).post
;
997 foreach_reverse (immutable idx
, FuiDeskWindow tw
; wintoplist
) {
998 if (evt
.source
is tw
) {
1000 foreach (immutable c
; idx
+1..wintoplist
.length
) wintoplist
[c
-1] = wintoplist
[c
];
1001 wintoplist
[$-1] = null;
1002 wintoplist
.length
-= 1;
1003 wintoplist
.assumeSafeAppend
;
1004 if (auto fcs
= lastFocus
.object
) {
1005 if (fcs
.toplevel
is tw
) topwindowFocusJustChanged();
1008 if (winlist
.length
== 0 && wintoplist
.length
== 0) (new FuiEventQuit
).post
;
1012 if (winlist
.length
== 0 && wintoplist
.length
== 0) (new FuiEventQuit
).post
;
1015 void onEvent (FuiEventWinFocusPrev evt
) {
1016 if (auto win
= evt
.sourcewin
) {
1017 if (win
.desk
!is this) return;
1018 auto nfc
= win
.findPrevToFocus();
1019 if (nfc
is null) nfc
= win
.findLastToFocus();
1020 if (nfc
!is null) switchFocusTo(nfc
);
1024 void onEvent (FuiEventWinFocusNext evt
) {
1025 if (auto win
= evt
.sourcewin
) {
1026 if (win
.desk
!is this) return;
1027 auto nfc
= win
.findNextToFocus();
1028 if (nfc
is null) nfc
= win
.findFirstToFocus();
1029 if (nfc
!is null) switchFocusTo(nfc
);
1033 @property FuiControl
focused () { return lastFocus
.object
; }
1034 @property void focused (FuiControl ctl
) { switchFocusTo(ctl
); }
1038 __gshared FuiEventQueueDesk tuidesk
;
1040 shared static this () {
1041 tuidesk
= new FuiEventQueueDesk();
1044 shared static ~this () {