CrabUI
Loading...
Searching...
No Matches
CUIMainComponent.cs
1#define SHOWPERF
2
3using System;
4using System.Collections.Generic;
5using System.Linq;
6using System.Reflection;
7
8using Barotrauma;
9using Microsoft.Xna.Framework;
10using Microsoft.Xna.Framework.Input;
11using Microsoft.Xna.Framework.Graphics;
12using System.Diagnostics;
13
14namespace CrabUI
15{
16 /// <summary>
17 /// Orchestrating drawing and updating of it's children
18 /// Also a CUIComponent, but it's draw and update methods
19 /// Attached directly to games life cycle
20 /// </summary>
22 {
23 public record DrawEvent(CUIComponent Target, bool front = false);
24
25 /// <summary>
26 /// Wrapper for global events
27 /// </summary>
28 public class CUIGlobalEvents
29 {
30 public Action<CUIInput> OnMouseDown; public void InvokeOnMouseDown(CUIInput e) => OnMouseDown?.Invoke(e);
31 public Action<CUIInput> OnMouseUp; public void InvokeOnMouseUp(CUIInput e) => OnMouseUp?.Invoke(e);
32 public Action<CUIInput> OnMouseMoved; public void InvokeOnMouseMoved(CUIInput e) => OnMouseMoved?.Invoke(e);
33 public Action<CUIInput> OnClick; public void InvokeOnClick(CUIInput e) => OnClick?.Invoke(e);
34 public Action<CUIInput> OnKeyDown; public void InvokeOnKeyDown(CUIInput e) => OnKeyDown?.Invoke(e);
35 public Action<CUIInput> OnKeyUp; public void InvokeOnKeyUp(CUIInput e) => OnKeyUp?.Invoke(e);
36 }
37
38 /// <summary>
39 /// Frozen window doesn't update
40 /// </summary>
41 public bool Frozen { get; set; }
42 public double UpdateInterval = 1.0 / 300.0;
43 /// <summary>
44 /// If true will update layout until it settles to prevent blinking
45 /// </summary>
46 public bool CalculateUntilResolved = true;
47 /// <summary>
48 /// If your GUI needs more than this steps of layout update
49 /// you will get a warning
50 /// </summary>
52 public event Action OnTreeChanged;
53 public Action AddOnTreeChanged { set { OnTreeChanged += value; } }
54
55 public CUIDragHandle GrabbedDragHandle;
56 public CUIResizeHandle GrabbedResizeHandle;
57 public CUISwipeHandle GrabbedSwipeHandle;
58 public CUIComponent MouseOn;
59 public CUIComponent FocusedComponent
60 {
61 get => CUI.FocusedComponent;
62 set => CUI.FocusedComponent = value;
63 }
64 /// <summary>
65 /// Container for true global events
66 /// CUIMainComponent itself can react to events and you can listen for those,
67 /// but e.g. mouse events may be consumed before they reach Main
68 /// </summary>
70
71 private Stopwatch sw = new Stopwatch();
72
73 internal List<CUIComponent> Flat = new();
74 internal List<CUIComponent> Leaves = new();
75 internal List<DrawEvent> DrawEvents = new();
76 internal SortedList<int, List<CUIComponent>> Layers = new();
77 private List<CUIComponent> MouseOnList = new();
78 private Vector2 GrabbedOffset;
79
80 private void RunStraigth(Action<CUIComponent> a) { for (int i = 0; i < Flat.Count; i++) a(Flat[i]); }
81 private void RunReverse(Action<CUIComponent> a) { for (int i = Flat.Count - 1; i >= 0; i--) a(Flat[i]); }
82
83
84
85
86 private void FlattenTree()
87 {
88 int retries = 0;
89 bool done = false;
90 do
91 {
92 retries++;
93 if (retries > 10) break;
94 try
95 {
96 Flat.Clear();
97 Layers.Clear();
98 // DrawEvents.Clear();
99
100 int globalIndex = 0;
101 void CalcZIndexRec(CUIComponent component, int added = 0)
102 {
103 component.positionalZIndex = globalIndex;
104 globalIndex += 1;
105 component.addedZIndex = added;
106 if (component.ZIndex.HasValue) component.addedZIndex += component.ZIndex.Value;
107
108 foreach (CUIComponent child in component.Children)
109 {
110 CalcZIndexRec(child, component.addedZIndex);
111 }
112 }
113
114 CalcZIndexRec(this, 0);
115 RunRecursiveOn(this, (c) =>
116 {
117 int i = c.positionalZIndex + c.addedZIndex;
118 if (!Layers.ContainsKey(i)) Layers[i] = new List<CUIComponent>();
119 Layers[i].Add(c);
120 });
121
122 foreach (var layer in Layers)
123 {
124 Flat.AddRange(layer.Value);
125 }
126
127 done = true;
128 }
129 catch (Exception e)
130 {
131 CUI.Warning($"Couldn't Flatten component tree: {e.Message}");
132 }
133 } while (!done);
134 }
135
136 #region Update
137
138 internal bool GlobalLayoutChanged;
139 internal void LayoutChanged() => GlobalLayoutChanged = true;
140 private double LastUpdateTime;
141 private int UpdateLoopCount = 0;
142 /// <summary>
143 /// Forses 1 layout update step, even when Frozen
144 /// </summary>
145 public void Step()
146 {
147 Update(LastUpdateTime + UpdateInterval, true, true);
148 }
149 public void Update(double totalTime, bool force = false, bool noInput = false)
150 {
151 if (!force)
152 {
153 if (Frozen) return;
154 if (totalTime - LastUpdateTime <= UpdateInterval) return;
155 }
156
157 CUIDebug.Flush();
158
159 if (TreeChanged)
160 {
161 OnTreeChanged?.Invoke();
162
163 FlattenTree();
164 TreeChanged = false;
165 }
166
167 if (!noInput) HandleInput(totalTime);
168
169 RunStraigth(c => c.InvokeOnUpdate(totalTime));
170
171
173 {
174 UpdateLoopCount = 0;
175 do
176 {
177 GlobalLayoutChanged = false;
178
179 if (TreeChanged)
180 {
181 OnTreeChanged?.Invoke();
182
183 FlattenTree();
184 TreeChanged = false;
185 }
186
187 RunReverse(c =>
188 {
189 c.Layout.ResizeToContent();
190 });
191
192 RunStraigth(c =>
193 {
194 c.Layout.Update();
195 c.Layout.UpdateDecor();
196 });
197
198 UpdateLoopCount++;
199 if (UpdateLoopCount >= MaxLayoutRecalcLoopsPerUpdate)
200 {
201 PrintRecalLimitWarning();
202 break;
203 }
204 }
205 while (GlobalLayoutChanged);
206 //CUI.Log($"UpdateLoopCount: {UpdateLoopCount}");
207 }
208 else
209 {
210 RunReverse(c =>
211 {
212 c.Layout.ResizeToContent();
213 });
214
215 RunStraigth(c =>
216 {
217 c.Layout.Update();
218 c.Layout.UpdateDecor();
219 });
220 }
221
222 //TODO do i need 2 updates?
223 //RunStraigth(c => c.InvokeOnUpdate(totalTime));
224
225 LastUpdateTime = totalTime;
226 }
227
228 #endregion
229 #region Draw
230
231 private void StopStart(SpriteBatch spriteBatch, Rectangle SRect, SamplerState? samplerState = null)
232 {
233 samplerState ??= GUI.SamplerState;
234 spriteBatch.End();
235 spriteBatch.GraphicsDevice.ScissorRectangle = SRect;
236 spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: samplerState, rasterizerState: GameMain.ScissorTestEnable);
237 }
238
239 public new void Draw(SpriteBatch spriteBatch)
240 {
241 sw.Restart();
242
243 Rectangle OriginalSRect = spriteBatch.GraphicsDevice.ScissorRectangle;
244 Rectangle SRect = OriginalSRect;
245
246 try
247 {
248 RunStraigth(c =>
249 {
250 if (!c.Visible || c.CulledOut) return;
251 if (c.Parent != null && c.Parent.ScissorRect.HasValue && SRect != c.Parent.ScissorRect.Value)
252 {
253 SRect = c.Parent.ScissorRect.Value;
254 StopStart(spriteBatch, SRect, c.SamplerState);
255 }
256 c.Draw(spriteBatch);
257 });
258 }
259 finally
260 {
261 if (spriteBatch.GraphicsDevice.ScissorRectangle != OriginalSRect) StopStart(spriteBatch, OriginalSRect);
262 }
263
264 RunStraigth(c =>
265 {
266 if (!c.Visible || c.CulledOut) return;
267 c.DrawFront(spriteBatch);
268 });
269
270 sw.Stop();
271 // CUIDebug.EnsureCategory();
272 // CUIDebug.CaptureTicks(sw.ElapsedTicks, "CUI.Draw");
273 }
274 #endregion
275 // https://youtu.be/xuFgUmYCS8E?feature=shared&t=72
276 #region HandleInput Start
277
278 public void OnDragEnd(CUIDragHandle h) { if (h == GrabbedDragHandle) GrabbedDragHandle = null; }
279 public void OnResizeEnd(CUIResizeHandle h) { if (h == GrabbedResizeHandle) GrabbedResizeHandle = null; }
280 public void OnSwipeEnd(CUISwipeHandle h) { if (h == GrabbedSwipeHandle) GrabbedSwipeHandle = null; }
281
282
283 private void HandleInput(double totalTime)
284 {
285 HandleGlobal(totalTime);
286 HandleMouse(totalTime);
287 HandleKeyboard(totalTime);
288 }
289
290 private void HandleGlobal(double totalTime)
291 {
292 if (CUI.Input.MouseDown) Global.InvokeOnMouseDown(CUI.Input);
293 if (CUI.Input.MouseUp)
294 {
295 Global.InvokeOnMouseUp(CUI.Input);
296 Global.InvokeOnClick(CUI.Input);
297 }
298 if (CUI.Input.MouseMoved) Global.InvokeOnMouseMoved(CUI.Input);
299 if (CUI.Input.SomeKeyPressed) Global.InvokeOnKeyDown(CUI.Input);
300 if (CUI.Input.SomeKeyUnpressed) Global.InvokeOnKeyUp(CUI.Input);
301 }
302
303 private void HandleKeyboard(double totalTime)
304 {
305 if (FocusedComponent == null) FocusedComponent = this;
306 if (CUI.Input.PressedKeys.Contains(Keys.Escape)) FocusedComponent = this;
307 if (CUI.Input.SomeKeyPressed) FocusedComponent.InvokeOnKeyDown(CUI.Input);
308 if (CUI.Input.SomeKeyUnpressed) FocusedComponent.InvokeOnKeyUp(CUI.Input);
309 if (CUI.Input.SomeWindowEvents) FocusedComponent.InvokeOnTextInput(CUI.Input);
310 }
311
312 private void HandleMouse(double totalTime)
313 {
314 if (!CUI.Input.SomethingHappened) return;
315
316 if (!CUI.Input.MouseHeld)
317 {
318 GrabbedDragHandle?.EndDrag();
319 GrabbedResizeHandle?.EndResize();
320 GrabbedSwipeHandle?.EndSwipe();
321 }
322
323 if (CUI.Input.MouseMoved)
324 {
325 GrabbedDragHandle?.DragTo(CUI.Input.MousePosition);
326 GrabbedResizeHandle?.Resize(CUI.Input.MousePosition);
327 GrabbedSwipeHandle?.Swipe(CUI.Input);
328 }
329
330 if (CUI.Input.MouseInputHandled) return;
331
332 //HACK
333 //if (CUI.Input.ClickConsumed) return;
334
335 //TODO think where should i put it?
336 if (GrabbedResizeHandle != null || GrabbedDragHandle != null || GrabbedSwipeHandle != null) return;
337
338 List<CUIComponent> prevMouseOnList = new List<CUIComponent>(MouseOnList);
339
340 CUIComponent CurrentMouseOn = null;
341 MouseOnList.Clear();
342
343 // form MouseOnList
344 // Note: including main component
345 if (
346 GUI.MouseOn == null || (GUI.MouseOn is GUIButton btn && btn.Text == "DUMMY")
347 || (this == CUI.TopMain) //TODO guh
348 )
349 {
350 RunStraigth(c =>
351 {
352 bool ok = !c.IgnoreEvents && c.Real.Contains(CUI.Input.MousePosition) && c.ShouldInvoke(CUI.Input);
353
354 if (c.Parent != null && c.Parent.ScissorRect.HasValue &&
355 !c.Parent.ScissorRect.Value.Contains(CUI.Input.Mouse.Position))
356 {
357 ok = false;
358 }
359
360 if (ok) MouseOnList.Add(c);
361 });
362 }
363
364 MouseOn = MouseOnList.LastOrDefault();
365
366 //HACK
367 if (MouseOn != this)
368 {
369 CUI.Input.MouseInputHandled = true;
370 CUIMultiModResolver.MarkOtherInputsAsHandled();
371 }
372
373 //if (CurrentMouseOn != null) GUI.MouseOn = dummyComponent;
374
375
376 foreach (CUIComponent c in prevMouseOnList)
377 {
378 c.MousePressed = false;
379 c.MouseOver = false;
380 c.InvokeOnMouseOff(CUI.Input);
381 }
382
383 //TODO Should i use ConsumeMouseClicks here?
384 for (int i = MouseOnList.Count - 1; i >= 0; i--)
385 {
386 MouseOnList[i].MousePressed = CUI.Input.MouseHeld;
387 MouseOnList[i].MouseOver = true;
388 MouseOnList[i].InvokeOnMouseOn(CUI.Input);
389
390 if (MouseOnList[i].ConsumeMouseClicks) break;
391 }
392
393 // Mouse enter / leave
394 foreach (CUIComponent c in prevMouseOnList.Except(MouseOnList)) c.InvokeOnMouseLeave(CUI.Input);
395 foreach (CUIComponent c in MouseOnList.Except(prevMouseOnList)) c.InvokeOnMouseEnter(CUI.Input);
396
397
398 // focus
399 if (CUI.Input.MouseDown)
400 {
401 CUIComponent newFocused = this;
402 for (int i = MouseOnList.Count - 1; i >= 0; i--)
403 {
404 if (MouseOnList[i].FocusHandle.ShouldStart(CUI.Input))
405 {
406 newFocused = MouseOnList[i];
407 break;
408 }
409 }
410 FocusedComponent = newFocused;
411 }
412
413 // Resize
414 for (int i = MouseOnList.Count - 1; i >= 0; i--)
415 {
416 if (MouseOnList[i].RightResizeHandle.ShouldStart(CUI.Input))
417 {
418 GrabbedResizeHandle = MouseOnList[i].RightResizeHandle;
419 GrabbedResizeHandle.BeginResize(CUI.Input.MousePosition);
420 break;
421 }
422
423 if (MouseOnList[i].LeftResizeHandle.ShouldStart(CUI.Input))
424 {
425 GrabbedResizeHandle = MouseOnList[i].LeftResizeHandle;
426 GrabbedResizeHandle.BeginResize(CUI.Input.MousePosition);
427 break;
428 }
429 }
430 if (GrabbedResizeHandle != null) return;
431
432 //Scroll
433 for (int i = MouseOnList.Count - 1; i >= 0; i--)
434 {
435 if (CUI.Input.Scrolled) MouseOnList[i].InvokeOnScroll(CUI.Input);
436
437 if (MouseOnList[i].ConsumeMouseScroll) break;
438 }
439
440 //Move
441 if (CUI.Input.MouseMoved)
442 {
443 for (int i = MouseOnList.Count - 1; i >= 0; i--)
444 {
445 MouseOnList[i].InvokeOnMouseMove(CUI.Input);
446 }
447 }
448
449
450
451 //Clicks
452 for (int i = MouseOnList.Count - 1; i >= 0; i--)
453 {
454 if (CUI.Input.MouseDown) MouseOnList[i].InvokeOnMouseDown(CUI.Input);
455 if (CUI.Input.MouseUp)
456 {
457 MouseOnList[i].InvokeOnMouseUp(CUI.Input);
458 MouseOnList[i].InvokeOnClick(CUI.Input);
459 }
460 if (CUI.Input.DoubleClick) MouseOnList[i].InvokeOnDClick(CUI.Input);
461
462 if (MouseOnList[i].ConsumeMouseClicks || CUI.Input.ClickConsumed) break;
463 }
464 if (CUI.Input.ClickConsumed) return;
465
466 // Swipe
467 for (int i = MouseOnList.Count - 1; i >= 0; i--)
468 {
469 if (MouseOnList[i].SwipeHandle.ShouldStart(CUI.Input))
470 {
471 GrabbedSwipeHandle = MouseOnList[i].SwipeHandle;
472 GrabbedSwipeHandle.BeginSwipe(CUI.Input.MousePosition);
473 break;
474 }
475
476 if (MouseOnList[i].ConsumeSwipe) break;
477 }
478 if (GrabbedSwipeHandle != null) return;
479
480 // Drag
481 for (int i = MouseOnList.Count - 1; i >= 0; i--)
482 {
483 if (MouseOnList[i].DragHandle.ShouldStart(CUI.Input))
484 {
485 GrabbedDragHandle = MouseOnList[i].DragHandle;
486 GrabbedDragHandle.BeginDrag(CUI.Input.MousePosition);
487 break;
488 }
489
490 if (MouseOnList[i].ConsumeDragAndDrop) break;
491 }
492 if (GrabbedDragHandle != null) return;
493 }
494 #endregion
495 #region HandleInput End
496 #endregion
497
498 /// <summary>
499 /// Obsolete function
500 /// Will run generator func with this
501 /// </summary>
502 /// <param name="initFunc"> Generator function that adds components to passed Main </param>
503 public void Load(Action<CUIMainComponent> initFunc)
504 {
505 RemoveAllChildren();
506 initFunc(this);
507 }
508
509 public CUIMainComponent() : base()
510 {
511 CullChildren = true;
512 Real = new CUIRect(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight);
513 Visible = false;
514 //IgnoreEvents = true;
516
517
518 Debug = true;
520 }
521
522 public void PrintRecalLimitWarning()
523 {
524 CUI.Log($"Warning: Your GUI code requires {MaxLayoutRecalcLoopsPerUpdate} layout update loops to fully resolve (which is cringe). Optimize it!", Color.Orange);
525 }
526 }
527}
Base class for all components.
SamplerState SamplerState
don't
CUIRect Real
Calculated prop, position on real screen in pixels Should be fully calculated after CUIMainComponent....
Func< CUIRect, CUIBoundaries > ChildrenBoundaries
Limits to children positions.
bool CullChildren
if child rect doesn't intersect with parent it won't be drawn and won't consume fps It also sets Hi...
bool Visible
Invisible components are not drawn, but still can be interacted with.
bool ShouldPassPropsToChildren
Some props (like visible) are autopassed to all new childs see PassPropsToChild.
static void RunRecursiveOn(CUIComponent component, Action< CUIComponent > action)
designed to be versatile, in fact never used
In fact a static class managing static things.
Definition CUIErrors.cs:16
Containing a snapshot of current mouse and keyboard state.
Definition CUIInput.cs:17
Orchestrating drawing and updating of it's children Also a CUIComponent, but it's draw and update m...
new void Draw(SpriteBatch spriteBatch)
Here component should be drawn.
CUIGlobalEvents Global
Container for true global events CUIMainComponent itself can react to events and you can listen for t...
bool CalculateUntilResolved
If true will update layout until it settles to prevent blinking.
bool Frozen
Frozen window doesn't update.
void Load(Action< CUIMainComponent > initFunc)
Obsolete function Will run generator func with this.
int MaxLayoutRecalcLoopsPerUpdate
If your GUI needs more than this steps of layout update you will get a warning.
void Step()
Forses 1 layout update step, even when Frozen.
Defining Boundaries, not the same as rect containing min/max x, y, z.
Rectangle with float.
Definition CUIRect.cs:17