egra: agg mini clipping bugfix; some fixes in widget rendering (buttons, etc.)
[iv.d.git] / tuing / tui.d
blob851222f67059dc00708c107a42c473329e9a057f
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*/;
21 import iv.alice;
22 import iv.eventbus;
23 import iv.flexlayout;
24 import iv.strex;
25 import iv.rawtty;
26 import iv.weakref;
28 import iv.tuing.types;
29 import iv.tuing.tty;
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 // ////////////////////////////////////////////////////////////////////////// //
42 struct FuiPalette {
43 uint def; // default color
44 uint sel; // sel is also focus
45 uint mark; // marked text
46 uint marksel; // active marked text
47 uint gauge; // unused
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
54 // hotkey
55 uint hot;
56 uint hotsel;
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
68 // sel is also focus
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
80 // hotkey
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 {
102 FuiControl ctl;
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 {
111 enum Flags : ubyte {
112 None = 0,
113 // UI flags
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;
124 ubyte flags;
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 () {
137 // setup groups
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];
148 return false;
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);
164 visible = true;
165 if (aparent !is null) {
166 lp.parent = aparent.lp;
167 auto lcc = aparent.lp.firstChild;
168 if (lcc is null) {
169 // first child
170 aparent.lp.firstChild = lp;
171 } else {
172 // non-first child
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) {
194 for (;;) {
195 if (auto nc = fcc.nextSibling) fcc = nc; else break;
197 return cast(typeof(return))fcc;
199 return null;
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) {
216 static if (withme) {
217 (new FuiEventClose(w, this)).post;
218 } else {
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
239 override {
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) {}
246 // flags accessors
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); }
312 protected {
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) {
323 return
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) {
330 if (v) {
331 clickMask |= cast(ubyte)(1<<(bt-TtyEvent.MButton.First));
332 } else {
333 clickMask &= cast(ubyte)~(1<<(bt-TtyEvent.MButton.First));
338 final bool canAcceptDouble (TtyEvent.MButton bt) {
339 return
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) {
346 if (v) {
347 doubleMask |= cast(ubyte)(1<<(bt-TtyEvent.MButton.First));
348 } else {
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) {
358 while (c !is null) {
359 if (auto cx = descend(c.firstChild)) return cx;
360 if (dg(c)) return c;
361 c = c.nextSibling;
363 return null;
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;
371 return null;
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]; }
384 void doAction () {
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;
394 return false;
397 protected void drawChildren (XtWindow win) {
398 if (lp.firstChild is null) return;
399 // setup scissoring
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);
423 } else {
424 onDraw(this, win);
428 public void draw (XtWindow win) {
429 if (!lp.visible) return;
430 if (lp.size.w < 1 || lp.size.h < 1) return;
431 // setup scissoring
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));
440 ttyScissor = csc;
441 drawChildren(XtWindow(mgb.x, mgb.y, lp.size.w, lp.size.h));
442 ttyScissor = csc;
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 {
462 protected:
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...
470 public:
471 this () {
472 lastHover = new Weak!FuiControl();
473 lastFocus = new Weak!FuiControl();
474 foreach (ref lcc; lastClick) lcc = new Weak!FuiControl();
477 // `pt` is global
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;
487 lho = null;
489 auto nh = atXY(lastMouse);
490 if (nh !is null && (nh.hidden || nh.disabled)) nh = null;
491 if (nh !is lho) {
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;
503 if (key.mouse) {
504 // fix hovering
505 auto pt = FuiPoint(key.x, key.y);
506 lastMouse = pt;
507 fixHovering();
508 // process buttons
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);
518 return true;
520 fixHovering(); // anyway, 'cause toplevel widget can be changed
521 if (auto fcs = lastFocus.object) {
522 // focus events
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;
527 return true;
529 return false;
532 private:
533 // [0..7]
534 void newButtonState (uint bidx, bool down) {
535 // beventCount:
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() () {
553 resetActive();
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;
560 return;
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;
571 return;
573 // this is invalid doubleclick, revert to simple click
574 beventCount[bidx] = 1;
575 // start registering doubleclicks
576 lastClickDelta[bidx] = 0;
578 // try single click
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;
587 return;
589 // something unexpected, reset it all
590 lastClick[bidx].object = null;
591 beventCount[bidx] = 0;
592 lastClickDelta[bidx] = lastClickDelta[0].max;
595 void doPress() () {
596 // void?
597 auto lp = lastHover.object;
598 if (lp is null) {
599 // reset all
600 lastClick[bidx].object = null;
601 beventCount[bidx] = 0;
602 lastClickDelta[bidx] = lastClickDelta[0].max;
603 return;
605 // first press?
606 if (beventCount[bidx] == 0) {
607 // start single
608 lastClick[bidx].object = lp;
609 beventCount[bidx] = 1;
610 lastClickDelta[bidx] = lastClickDelta[0].max;
611 // change focus
612 if (lp.canBeFocused) switchFocusTo(lp);
613 if ((lp.clickMask&(1<<bidx)) != 0) (new FuiEventActive(lp)).post;
614 return;
616 // second press?
617 if (beventCount[bidx] == 2) {
618 // start double if control is the same
619 if (lastClick[bidx].object is lp) {
620 // same
621 if (lastClickDelta[bidx] > fuiDoubleTime) {
622 // reset double to single
623 beventCount[bidx] = 1;
624 lastClickDelta[bidx] = lastClickDelta[0].max;
625 } else {
626 beventCount[bidx] = 3;
628 } else {
629 // other, reset to "first press"
630 lastClick[bidx].object = lp;
631 beventCount[bidx] = 1;
632 lastClickDelta[bidx] = lastClickDelta[0].max;
634 // change focus
635 if (lp.canBeFocused) switchFocusTo(lp);
636 if (((lp.doubleMask|lp.clickMask)&(1<<bidx)) != 0) (new FuiEventActive(lp)).post;
637 return;
639 resetActive();
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;
647 if (down) {
648 // button pressed
649 if ((lastButtons&(1<<bidx)) != 0) return; // state didn't changed
650 lastButtons |= cast(ubyte)(1<<bidx);
651 doPress();
652 } else {
653 // button released
654 if ((lastButtons&(1<<bidx)) == 0) return; // state didn't changed
655 lastButtons &= cast(ubyte)~(1<<bidx);
656 doRelease();
662 // ////////////////////////////////////////////////////////////////////////// //
663 class FuiEventQueueDesk : FuiEventQueue {
664 static struct WinInfo {
665 enum Type { Normal, Modal, Popup }
666 FuiDeskWindow win;
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 {
679 TtyEvent[] combo;
680 void delegate () handler;
682 HotKey[] hkcombos;
683 TtyEvent[] hkcurcombo;
685 final TtyEvent[] parseHotCombo (TtyEvent[] dest, const(char)[] hkstr) {
686 int cp = 0;
687 TtyEvent key;
688 auto ostr = 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;
696 return dest[0..cp];
699 final bool hasHotKey (const(char)[] hkstr) {
700 TtyEvent[64] cbuf;
701 try {
702 auto cb = parseHotCombo(cbuf[], hkstr);
703 foreach (const ref hk; hkcombos) if (hk.combo == cb) return true;
704 } catch (Exception) {
706 return false;
709 final bool removeHotKey (const(char)[] hkstr) {
710 TtyEvent[64] cbuf;
711 try {
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;
719 return true;
722 } catch (Exception) {
724 return false;
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");
730 TtyEvent[64] cbuf;
731 auto cb = parseHotCombo(cbuf[], hkstr);
732 foreach (ref hk; hkcombos) {
733 if (hk.combo == cb) {
734 static if (allowOverride) hk.handler = hh;
735 return true;
738 hkcombos ~= HotKey(cb.dup, hh);
739 return false;
742 final WinInfo* findWinInfo (FuiDeskWindow w) {
743 foreach_reverse (ref WinInfo wi; winlist) if (wi.win is w) return &wi;
744 return null;
747 final bool isOnTopWindow (FuiControl ctl) {
748 if (ctl is null) return false;
749 ctl = ctl.toplevel;
750 foreach_reverse (FuiControl w; wintoplist) if (w is ctl) return true;
751 return false;
754 final bool isNormalWindow (FuiControl ctl) {
755 if (ctl is null) return false;
756 ctl = ctl.toplevel;
757 foreach_reverse (ref WinInfo w; winlist) if (w.win is ctl) return true;
758 return false;
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);
767 // `pt` is global
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) {
772 pt -= ctl.lp.pos;
773 if (pt.x >= 0 && pt.y >= 0 && pt.x < ctl.lp.size.w && pt.y < ctl.lp.size.h) {
774 lasthit = ctl;
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;
782 return lasthit;
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);
792 return null;
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) {
822 if (wi.win is tl) {
823 if (wi.shouldCloseOnBlur) (new FuiEventClose(wi.win, null)).post; else (new FuiEventBlur(wi.win)).post;
824 break;
830 // old active window is blurred, focus new one
831 if (winlist.length == 0) return;
832 auto w = winlist[$-1].win;
833 assert(w !is null);
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
847 // fix focus
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;
852 if (ctl is win) {
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]) {
866 if (wi.win is win) {
867 auto xwi = wi;
868 foreach (immutable c; idx+1..winlist.length) winlist[c-1] = winlist[c];
869 winlist[$-1] = xwi;
870 if (realfct.parent !is null) winlist[$-1].win.lastfct = realfct;
871 topwindowFocusJustChanged(winlist[$-2]);
872 return;
875 ttyBeep; // error!
876 return;
878 // remove focus from current focused ctl
879 if (ofc !is null) {
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;
883 ofc = null;
885 // focus new control
886 lastFocus.object = realfct;
887 if (realfct !is win) {
888 win.lastfct = realfct;
889 (new FuiEventFocus(ctl)).post;
893 this () {
894 this.connectListeners();
895 super();
898 protected void addWindowWithType (FuiDeskWindow w, WinInfo.Type type) {
899 if (w is null) return;
900 if (w.desk !is null) return;
901 w.desk = this;
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) {
912 // check hotkeys
913 if (hkcombos.length) {
914 hkcurcombo ~= key;
915 bool wasHit;
916 foreach (ref hk; hkcombos) {
917 if (hk.combo == hkcurcombo) {
918 hkcurcombo.length = 0;
919 hkcurcombo.assumeSafeAppend;
920 hk.handler();
921 return true;
922 } else if (!wasHit && hk.combo.length > hkcurcombo.length && hk.combo[0..hkcurcombo.length] == hkcurcombo) {
923 wasHit = true;
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]) {
945 auto w = wi.win;
946 if (w.hidden || w.disabled) continue;
947 if (pt.inside(w.lp.rect)) {
948 auto lastWF = winlist[$-1];
949 auto xwi = wi;
950 foreach (immutable c; idx+1..winlist.length) winlist[c-1] = winlist[c];
951 winlist[$-1] = xwi;
952 topwindowFocusJustChanged(lastWF);
953 break;
958 return super.queue(key);
961 void draw () {
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');
967 } else {
968 drawDesk(this);
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;
978 } else {
979 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) {
985 // i found her!
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;
989 winlist.length -= 1;
990 winlist.assumeSafeAppend;
991 if (lastWF.win !is null) { topwindowFocusJustChanged(lastWF); lastWF.win.desk = null; }
992 tw.desk = null;
993 if (winlist.length == 0 && wintoplist.length == 0) (new FuiEventQuit).post;
994 return;
997 foreach_reverse (immutable idx, FuiDeskWindow tw; wintoplist) {
998 if (evt.source is tw) {
999 // i found her!
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();
1007 tw.desk = null;
1008 if (winlist.length == 0 && wintoplist.length == 0) (new FuiEventQuit).post;
1009 return;
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 () {
1045 tuidesk = null;