1/*
2 Copyright (c) 2013 yvt
3
4 This file is part of OpenSpades.
5
6 OpenSpades is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 OpenSpades 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.
15
16 You should have received a copy of the GNU General Public License
17 along with OpenSpades.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21namespace spades {
22	namespace ui {
23
24		funcdef void EventHandler(UIElement@ sender);
25		funcdef void PasteClipboardEventHandler(string text);
26
27		/** Manages all input/output and rendering of the UI framework. */
28		class UIManager {
29			private Renderer@ renderer;
30			private AudioDevice@ audioDevice;
31
32			Vector2 MouseCursorPosition;
33			UIElement@ RootElement;
34			UIElement@ ActiveElement;
35			Cursor@ DefaultCursor;
36			float Time = 0.f;
37			bool IsControlPressed = false;
38			bool IsShiftPressed = false;
39			bool IsAltPressed = false;
40			bool IsMetaPressed = false;
41
42			// IME (Input Method Editor) support
43			string EditingText;
44			int EditingStart = 0;
45			int EditingLength = 0;
46
47			private UIElement@ mouseCapturedElement;
48			private UIElement@ mouseHoverElement;
49
50			private PasteClipboardEventHandler@ clipboardEventHandler;
51
52			private Timer@[] timers = {};
53			private KeyRepeatManager keyRepeater;
54			private KeyRepeatManager charRepeater;
55
56			Renderer@ Renderer {
57				get final { return renderer; }
58			}
59
60			AudioDevice@ AudioDevice {
61				get final { return audioDevice; }
62			}
63
64			UIManager(Renderer@ renderer, AudioDevice@ audioDevice) {
65				@this.renderer = renderer;
66				@this.audioDevice = audioDevice;
67
68				@RootElement = UIElement(this);
69				RootElement.Size = Vector2(renderer.ScreenWidth, renderer.ScreenHeight);
70
71				@DefaultCursor = Cursor(this, renderer.RegisterImage("Gfx/UI/Cursor.png"), Vector2(8.f, 8.f));
72				MouseCursorPosition = Vector2(renderer.ScreenWidth * 0.5f, renderer.ScreenHeight * 0.5f);
73
74				@keyRepeater.handler = KeyRepeatEventHandler(this.HandleKeyInner);
75				@charRepeater.handler = KeyRepeatEventHandler(this.HandleCharInner);
76			}
77
78			private MouseButton TranslateMouseButton(string key){
79				if(key == "LeftMouseButton") {
80					return spades::ui::MouseButton::LeftMouseButton;
81				}else if(key == "RightMouseButton") {
82					return spades::ui::MouseButton::RightMouseButton;
83				}else if(key == "MiddleMouseButton") {
84					return spades::ui::MouseButton::MiddleMouseButton;
85				}else if(key == "MouseButton4") {
86					return spades::ui::MouseButton::MouseButton4;
87				}else if(key == "MouseButton5") {
88					return spades::ui::MouseButton::MouseButton5;
89				}else {
90					return spades::ui::MouseButton::None;
91				}
92			}
93
94			private UIElement@ GetMouseActiveElement() {
95				if(mouseCapturedElement !is null){
96					return mouseCapturedElement;
97				}
98				if(not RootElement.Enable) {
99					return null;
100				}
101				UIElement@ elm = RootElement.MouseHitTest(MouseCursorPosition);
102				return elm;
103			}
104
105			private Cursor@ GetCurrentCursor() {
106				UIElement@ e = GetMouseActiveElement();
107				if(e is null) {
108					return DefaultCursor;
109				}
110				return e.Cursor;
111			}
112
113			void WheelEvent(float x, float y) {
114				UIElement@ e = GetMouseActiveElement();
115				if(e !is null) {
116					e.MouseWheel(y);
117				}
118			}
119
120			private void MouseEventDone() {
121				UIElement@ e = GetMouseActiveElement();
122				if(e !is null) {
123					e.MouseMove(e.ScreenToClient(MouseCursorPosition));
124				}
125
126				// check for mouse enter/leave
127				if(e is null) {
128					@e = RootElement.MouseHitTest(MouseCursorPosition);
129				}
130				if(e !is mouseHoverElement) {
131					if(mouseHoverElement !is null) {
132						mouseHoverElement.MouseLeave();
133					}
134					@mouseHoverElement = e;
135					if(e !is null) {
136						e.MouseEnter();
137					}
138				}
139			}
140
141			void MouseEvent(float x, float y) {
142				/*
143				MouseCursorPosition = Vector2(
144					Clamp(MouseCursorPosition.x + x, 0.f, renderer.ScreenWidth),
145					Clamp(MouseCursorPosition.y + y, 0.f, renderer.ScreenHeight)
146				);
147				*/
148				// in current version, absolute mouse mode is supported.
149				MouseCursorPosition = Vector2(
150					Clamp(x, 0.f, renderer.ScreenWidth),
151					Clamp(y, 0.f, renderer.ScreenHeight)
152				);
153
154				MouseEventDone();
155			}
156
157			// forces key repeat to stop.
158			void KeyPanic() {
159				keyRepeater.KeyUp();
160				charRepeater.KeyUp();
161				EditingText = "";
162				EditingStart = 0;
163				EditingLength = 0;
164				IsShiftPressed = false;
165				IsControlPressed = false;
166				IsAltPressed = false;
167				IsMetaPressed = false;
168			}
169
170			void KeyEvent(string key, bool down) {
171				if(key.length == 0){
172					if(!down) {
173						keyRepeater.KeyUp();
174						charRepeater.KeyUp();
175					}
176					return;
177				}
178				if(key == "Shift") {
179					IsShiftPressed = down;
180				}
181				if(key == "Control") {
182					IsControlPressed = down;
183				}
184				if(key == "Alt") {
185					IsAltPressed = down;
186				}
187				if(key == "Meta") {
188					IsMetaPressed = down;
189				}
190				if(key == "WheelUp") {
191					UIElement@ e = GetMouseActiveElement();
192					if(e !is null) {
193						e.MouseWheel(-1.f);
194					}
195					return;
196				}
197				if(key == "WheelDown") {
198					UIElement@ e = GetMouseActiveElement();
199					if(e !is null) {
200						e.MouseWheel(1.f);
201					}
202					return;
203				}
204
205				MouseButton mb = TranslateMouseButton(key);
206				if(mb != spades::ui::MouseButton::None) {
207					UIElement@ e = GetMouseActiveElement();
208					if(e !is null) {
209						if(down) {
210							@mouseCapturedElement = e;
211							if(e.AcceptsFocus) {
212								// give keyboard focus, too
213								@ActiveElement = e;
214							}
215							e.MouseDown(mb, e.ScreenToClient(MouseCursorPosition));
216						} else {
217							// FIXME: release the mouse capture when all button are released?
218							@mouseCapturedElement = null;
219							e.MouseUp(mb, e.ScreenToClient(MouseCursorPosition));
220							MouseEventDone();
221						}
222					}
223					return;
224				}
225
226				if(down) {
227					HandleKeyInner(key);
228					keyRepeater.KeyDown(key);
229				}else{
230					keyRepeater.KeyUp();
231					charRepeater.KeyUp();
232					UIElement@ e = ActiveElement;
233					if(e !is null) {
234						e.KeyUp(key);
235					}
236				}
237			}
238
239			void TextInputEvent(string chr) {
240				charRepeater.KeyDown(chr);
241				HandleCharInner(chr);
242			}
243
244			void TextEditingEvent(string chr, int start, int len) {
245				EditingText = chr;
246				EditingStart = GetByteIndexForString(chr, start);
247				EditingLength = GetByteIndexForString(chr, len, EditingStart) - EditingStart;
248			}
249
250			bool AcceptsTextInput {
251				get {
252					if(ActiveElement is null) {
253						EditingText = "";
254						EditingStart = 0;
255						EditingLength = 0;
256					}
257					return ActiveElement !is null;
258				}
259			}
260
261			AABB2 TextInputRect {
262				get {
263					UIElement@ e = ActiveElement;
264					if(e !is null) {
265						AABB2 rt = e.TextInputRect;
266						Vector2 off = e.ScreenPosition;
267						rt.min += off;
268						rt.max += off;
269						return rt;
270					}else{
271						return AABB2();
272					}
273				}
274			}
275
276			private void HandleKeyInner(string key) {
277				{
278					UIElement@ e = ActiveElement;
279					if(EditingText.length > 0) {
280						// now text is being composed by IME.
281						// ignore some keys to resolve confliction.
282						if(key == "Escape" || key == "BackSpace" || key == "Left" || key == "Right" ||
283						   key == "Space" || key == "Enter" || key == "Up" || key == "Down" ||
284						   key == "Tab") {
285						   	return;
286						}
287					}
288					if(e !is null) {
289						e.KeyDown(key);
290					} else {
291						ProcessHotKey(key);
292					}
293				}
294			}
295
296			private void HandleCharInner(string chr) {
297				UIElement@ e = ActiveElement;
298				if(e !is null) {
299					e.KeyPress(chr);
300				}
301			}
302
303
304			void ProcessHotKey(string key) {
305				if(RootElement.Visible) {
306					RootElement.HotKey(key);
307				}
308			}
309
310			void AddTimer(Timer@ timer) {
311				timers.insertLast(timer);
312			}
313
314			void RemoveTimer(Timer@ timer) {
315				int idx = -1;
316				Timer@[]@ t = timers;
317				for(int i = t.length - 1; i >= 0; i--){
318					if(t[i] is timer) {
319						idx = i;
320						break;
321					}
322				}
323				if(idx >= 0){
324					timers.removeAt(idx);
325				}
326			}
327
328			void PlaySound(string filename) {
329				if(audioDevice !is null) {
330					audioDevice.PlayLocal(audioDevice.RegisterSound(filename), AudioParam());
331				}
332			}
333
334			void RunFrame(float dt) {
335				if(ActiveElement !is null) {
336					if(not (ActiveElement.IsVisible and ActiveElement.IsEnabled)) {
337						@ActiveElement = null;
338					}
339				}
340
341				if(clipboardEventHandler !is null) {
342					if(GotClipboardData()) {
343						clipboardEventHandler(GetClipboardData());
344						@clipboardEventHandler = null;
345					}
346				}
347
348				Timer@[]@ timers = this.timers;
349				for(int i = timers.length - 1; i >= 0; i--) {
350					timers[i].RunFrame(dt);
351				}
352				keyRepeater.RunFrame(dt);
353				charRepeater.RunFrame(dt);
354				Time += dt;
355			}
356
357			void Render() {
358				if(RootElement.Visible) {
359					RootElement.Render();
360				}
361
362				// render cursor
363				Cursor@ c = GetCurrentCursor();
364				if(c !is null) {
365					c.Render(MouseCursorPosition);
366				}
367			}
368
369			void Copy(string text) {
370				SetClipboardData(text);
371			}
372
373			void Paste(PasteClipboardEventHandler@ handler) {
374				RequestClipboardData();
375				@clipboardEventHandler = handler;
376			}
377		}
378
379		funcdef void KeyRepeatEventHandler(string key);
380		class KeyRepeatManager {
381			string lastKey;
382			float nextDelay;
383			KeyRepeatEventHandler@ handler;
384
385			void KeyDown(string key) {
386				lastKey = key;
387				nextDelay = 0.2f;
388			}
389			void KeyUp() {
390				lastKey = "";
391			}
392
393			void RunFrame(float dt) {
394				if(lastKey.length == 0)
395					return;
396				nextDelay -= dt;
397				if(nextDelay < 0.f) {
398					handler(lastKey);
399					nextDelay = Max(nextDelay + 0.06f, 0.f);
400				}
401			}
402		}
403
404		funcdef void TimerTickEventHandler(Timer@);
405		class Timer {
406			private UIManager@ manager;
407			TimerTickEventHandler@ Tick;
408
409			/** Minimum interval with which the timer fires. */
410			float Interval = 1.f;
411
412			bool AutoReset = true;
413
414			private float nextDelay;
415
416			Timer(UIManager@ manager) {
417				@this.manager = manager;
418			}
419
420			UIManager@ Manager {
421				get final {
422					return manager;
423				}
424			}
425
426			void OnTick() {
427				if(Tick !is null) {
428					Tick(this);
429				}
430			}
431
432			/** Called by UIManager. */
433			void RunFrame(float dt) {
434				nextDelay -= dt;
435				if(nextDelay < 0.f) {
436					OnTick();
437					if(AutoReset) {
438						nextDelay = Max(nextDelay + Interval, 0.f);
439					} else {
440						Stop();
441					}
442				}
443			}
444
445			void Start() {
446				nextDelay = Interval;
447				manager.AddTimer(this);
448			}
449
450			void Stop() {
451				manager.RemoveTimer(this);
452			}
453
454		}
455
456		enum MouseButton {
457			None,
458			LeftMouseButton,
459			RightMouseButton,
460			MiddleMouseButton,
461			MouseButton4,
462			MouseButton5
463		}
464
465		class UIElementIterator {
466			private bool initial = true;
467			private UIElement@ e;
468			UIElementIterator(UIElement@ parent) {
469				@e = parent;
470			}
471			UIElement@ Current {
472				get { return initial ? null : e; }
473			}
474			bool MoveNext() {
475				if(initial) {
476					@e = e.FirstChild;
477					initial = false;
478				} else {
479					@e = e.NextSibling;
480				}
481				return @e !is null;
482			}
483		}
484
485		class UIElementReverseIterator {
486			private bool initial = true;
487			private UIElement@ e;
488			UIElementReverseIterator(UIElement@ parent) {
489				@e = parent;
490			}
491			UIElement@ Current {
492				get { return initial ? null : e; }
493			}
494			bool MoveNext() {
495				if(initial) {
496					@e = e.LastChild;
497					initial = false;
498				} else {
499					@e = e.PrevSibling;
500				}
501				return @e !is null;
502			}
503		}
504
505		class UIElement {
506			private UIManager@ manager;
507			private UIElement@ parent;
508			//private UIElement@[] children = {};
509			private UIElement@ firstChild, lastChild;
510			private UIElement@ prevSibling, nextSibling;
511			private Font@ fontOverride;
512			private Cursor@ cursorOverride;
513
514			EventHandler@ MouseEntered;
515			EventHandler@ MouseLeft;
516
517			bool Visible = true;
518			bool Enable = true;
519
520			/** When AcceptsFocus is set to true, this element can be activated when
521			 *  it receives a mouse event. */
522			bool AcceptsFocus = false;
523
524			/** When IsMouseInteractive is set to true, this element receives mouse events. */
525			bool IsMouseInteractive = false;
526
527			/** When this is set to true, all mouse interraction outside the client area is
528			 * ignored for all sub-elements, reducing the CPU load. The visual is not clipped. */
529			bool ClipMouse = true;
530
531			Vector2 Position;
532			Vector2 Size;
533
534
535			UIElement(UIManager@ manager) {
536				@this.manager = manager;
537			}
538
539			UIElement@ Parent {
540				get final { return parent; }
541				set final {
542					if(value is this.Parent){
543						return;
544					}
545
546					// remove from old container
547					if(parent !is null){
548						parent.RemoveChild(this);
549					}
550
551					// add to new container
552					if(value !is null){
553						value.AddChild(this);
554					}
555				}
556			}
557
558			UIManager@ Manager {
559				get final { return manager; }
560			}
561
562			// used by UIElementIterator. Do not use.
563			UIElement@ FirstChild {
564				get final { return firstChild; }
565			}
566			// used by UIElementIterator. Do not use.
567			UIElement@ LastChild {
568				get final { return lastChild; }
569			}
570
571			// used by UIElementIterator. Do not use.
572			UIElement@ NextSibling {
573				get final { return nextSibling; }
574			}
575			// used by UIElementIterator. Do not use.
576			UIElement@ PrevSibling {
577				get final { return prevSibling; }
578			}
579
580			UIElement@[]@ GetChildren() final {
581				UIElement@[] elems = {};
582				UIElement@ e = firstChild;
583				while(e !is null) {
584					elems.insertLast(e);
585					@e = e.nextSibling;
586				}
587				return elems;
588				//return array<spades::ui::UIElement@>(children);
589			}
590
591			void AppendChild(UIElement@ element) {
592
593				UIElement@ oldParent = element.Parent;
594				if(oldParent is this){
595					return;
596				}else if(oldParent !is null){
597					@element.Parent = null;
598				}
599
600				if(firstChild is null) {
601					@firstChild = element;
602					@lastChild = element;
603					@element.parent = this;
604				} else {
605					@element.prevSibling = @lastChild;
606					@lastChild.nextSibling = @element;
607					@lastChild = @element;
608					@element.parent = this;
609				}
610			}
611			void PrependChild(UIElement@ element) {
612
613				UIElement@ oldParent = element.Parent;
614				if(oldParent is this){
615					return;
616				}else if(oldParent !is null){
617					@element.Parent = null;
618				}
619
620				if(firstChild is null) {
621					@firstChild = element;
622					@lastChild = element;
623					@element.parent = this;
624				} else {
625					@element.nextSibling = @firstChild;
626					@firstChild.prevSibling = @element;
627					@firstChild = @element;
628					@element.parent = this;
629				}
630			}
631
632			void AddChild(UIElement@ element) {
633				AppendChild(element);
634			}
635
636			void RemoveChild(UIElement@ element) {
637				if(element.parent !is this) {
638					return;
639				}
640
641				if(firstChild is element) {
642					if(lastChild is element) {
643						@firstChild = null;
644						@lastChild = null;
645					} else {
646						@firstChild = element.nextSibling;
647						@firstChild.prevSibling = null;
648					}
649				} else if(lastChild is element) {
650					@lastChild = element.prevSibling;
651					@lastChild.nextSibling = null;
652				} else {
653					@element.prevSibling.nextSibling = @element.nextSibling;
654					@element.nextSibling.prevSibling = @element.prevSibling;
655				}
656				@element.prevSibling = null;
657				@element.nextSibling = null;
658				@element.parent = null;
659
660			/*
661				int index = -1;
662				UIElement@[]@ c = children;
663				for(int i = c.length - 1; i >= 0; i--) {
664					if(c[i] is element) {
665						index = i;
666						break;
667					}
668				}
669
670				if(index >= 0){
671					children.removeAt(index);
672					@element.parent = null;
673				}*/
674			}
675
676			Font@ Font {
677				get final {
678					if(fontOverride !is null) {
679						return fontOverride;
680					}
681					if(parent is null) {
682						return null;
683					}
684					return parent.Font;
685				}
686				set {
687					@fontOverride = value;
688				}
689			}
690
691			Cursor@ Cursor {
692				get final {
693					if(cursorOverride !is null) {
694						return cursorOverride;
695					}
696					if(parent is null) {
697						return Manager.DefaultCursor;
698					}
699					return parent.Cursor;
700				}
701				set {
702					@cursorOverride = value;
703				}
704			}
705
706			bool IsVisible {
707				get final {
708					if(not Visible) return false;
709					if(parent is null) return this is Manager.RootElement;
710					return parent.IsVisible;
711				}
712			}
713
714			bool IsEnabled {
715				get final {
716					if(not Enable) return false;
717					if(parent is null) return true;
718					return parent.IsEnabled;
719				}
720			}
721
722			bool IsFocused {
723				get final {
724					return manager.ActiveElement is this;
725				}
726			}
727
728			Vector2 ScreenPosition {
729				get final {
730					if(parent is null) {
731						return Position;
732					}
733					return Position + parent.ScreenPosition;
734				}
735			}
736
737			AABB2 Bounds {
738				get final {
739					return AABB2(Position, Position + Size);
740				}
741				set {
742					Position = value.min;
743					Size = value.max - value.min;
744					OnResized();
745				}
746			}
747
748			AABB2 ScreenBounds {
749				get final {
750					Vector2 screenPos = ScreenPosition;
751					return AABB2(screenPos, screenPos + Size);
752				}
753			}
754
755			// IME supports
756
757			AABB2 TextInputRect {
758				get {
759					return AABB2(0.f, 0.f, Size.x, Size.y);
760				}
761			}
762
763			int TextEditingRangeStart {
764				get final {
765					if(this.IsFocused) {
766						return Manager.EditingStart;
767					}else{
768						return 0;
769					}
770				}
771			}
772
773			int TextEditingRangeLength {
774				get final {
775					if(this.IsFocused) {
776						return Manager.EditingLength;
777					}else{
778						return 0;
779					}
780				}
781			}
782
783			string EditingText {
784				get final {
785					if(this.IsFocused) {
786						return Manager.EditingText;
787					}else{
788						return "";
789					}
790				}
791			}
792
793			void OnResized() {
794			}
795
796			// relativePos is parent relative
797			UIElement@ MouseHitTest(Vector2 relativePos) final {
798				if(ClipMouse) {
799					if(not Bounds.Contains(relativePos)) {
800						return null;
801					}
802				}
803
804
805				Vector2 p = relativePos - Position;
806
807				UIElementReverseIterator iterator(this);
808				while(iterator.MoveNext()) {
809					UIElement@ e = iterator.Current;
810					if(not (e.Visible and e.Enable)) {
811						continue;
812					}
813					UIElement@ elem = e.MouseHitTest(p);
814					if(elem !is null){
815						return elem;
816					}
817				}
818
819				if(IsMouseInteractive) {
820					if(Bounds.Contains(relativePos)) {
821						return this;
822					}
823				}
824
825				return null;
826			}
827
828			Vector2 ScreenToClient(Vector2 scr) final {
829				return scr - ScreenPosition;
830			}
831
832			void MouseWheel(float delta) {
833				if(Parent !is null) {
834					Parent.MouseWheel(delta);
835				}
836			}
837			void MouseDown(MouseButton button, Vector2 clientPosition) {
838				if(Parent !is null) {
839					Parent.MouseDown(button, clientPosition);
840				}
841			}
842			void MouseMove(Vector2 clientPosition) {
843				if(Parent !is null) {
844					Parent.MouseMove(clientPosition);
845				}
846			}
847			void MouseUp(MouseButton button, Vector2 clientPosition) {
848				if(Parent !is null) {
849					Parent.MouseUp(button, clientPosition);
850				}
851			}
852			void MouseEnter() {
853				if(MouseEntered !is null) {
854					MouseEntered(this);
855				}
856			}
857			void MouseLeave() {
858				if(MouseLeft !is null) {
859					MouseLeft(this);
860				}
861			}
862
863			void KeyDown(string key) {
864				manager.ProcessHotKey(key);
865			}
866			void KeyUp(string key) {
867			}
868
869			void KeyPress(string text) {
870			}
871
872			void HotKey(string key) {
873				UIElementReverseIterator iterator(this);
874				while(iterator.MoveNext()) {
875					if(iterator.Current.Visible and iterator.Current.Enable) {
876						iterator.Current.HotKey(key);
877					}
878				}
879			}
880
881			void Render() {
882				UIElementIterator iterator(this);
883				while(iterator.MoveNext()) {
884					if(iterator.Current.Visible) {
885						iterator.Current.Render();
886					}
887				}
888			}
889		}
890
891		class Cursor {
892			private UIManager@ manager;
893			private Image@ image;
894			private Vector2 hotSpot;
895
896			Cursor(UIManager@ manager, Image@ image, Vector2 hotSpot) {
897				@this.manager = manager;
898				@this.image = image;
899				this.hotSpot = hotSpot;
900			}
901
902			void Render(Vector2 pos) {
903				Renderer@ renderer = manager.Renderer;
904				renderer.ColorNP = Vector4(1.f, 1.f, 1.f, 1.f);
905				renderer.DrawImage(image, Vector2(pos.x - hotSpot.x, pos.y - hotSpot.y));
906			}
907		}
908
909
910		class UIElementDeque {
911			private UIElement@[]@ arr;
912			private int startIndex = 0;
913			private int count = 0;
914
915			UIElementDeque() {
916				@arr = array<spades::ui::UIElement@>(4);
917			}
918
919			private int MapIndex(int idx) const {
920				idx += startIndex;
921				if(idx >= int(arr.length))
922					idx -= int(arr.length);
923				return idx;
924			}
925
926			UIElement@ get_opIndex(int idx) const {
927				return arr[MapIndex(idx)];
928			}
929			void set_opIndex(int idx, UIElement@ e) {
930				@arr[MapIndex(idx)] = e;
931			}
932
933			void Reserve(int c) {
934				if(int(arr.length) >= c){
935					return;
936				}
937				int newCount = int(arr.length);
938				while(newCount < c) {
939					newCount *= 2;
940				}
941
942				UIElement@[] newarr = array<spades::ui::UIElement@>(newCount);
943				UIElement@[] oldarr = arr;
944				int len = int(oldarr.length);
945				int idx = startIndex;
946				for(int i = 0; i < count; i++) {
947					@newarr[i] = oldarr[idx];
948					idx += 1;
949					if(idx >= len) {
950						idx = 0;
951					}
952				}
953				@arr = newarr;
954				startIndex = 0;
955			}
956
957			void PushFront(UIElement@ elem) {
958				Reserve(count + 1);
959				startIndex = (startIndex == 0) ? (arr.length - 1) : (startIndex - 1);
960				count++;
961				@this.Front = elem;
962			}
963
964			void PushBack(UIElement@ elem) {
965				Reserve(count + 1);
966				count++;
967				@this.Back = elem;
968			}
969
970			void PopFront() {
971				@this.Front = null;
972				startIndex = MapIndex(1);
973				count--;
974			}
975
976			void PopBack() {
977				@this.Back = null;
978				count--;
979			}
980
981			int Count {
982				get { return count; }
983			}
984
985			void Clear() {
986				@arr = array<spades::ui::UIElement@>(4);
987				count = 0;
988				startIndex = 0;
989			}
990
991			UIElement@ Front {
992				get const { return arr[startIndex]; }
993				set { @arr[startIndex] = value; }
994			}
995			UIElement@ Back {
996				get const { return arr[MapIndex(count - 1)]; }
997				set { @arr[MapIndex(count - 1)] = value; }
998			}
999		}
1000	}
1001}