// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using FlaxEngine.Assertions; namespace FlaxEngine.GUI { /// /// Base class for all GUI controls that can contain child controls. /// public class ContainerControl : Control { /// /// The children collection. /// [NoSerialize] protected readonly List _children = new List(); /// /// The contains focus cached flag. /// [NoSerialize] protected bool _containsFocus; /// /// Initializes a new instance of the class. /// public ContainerControl() { IsLayoutLocked = true; } /// /// Initializes a new instance of the class. /// public ContainerControl(float x, float y, float width, float height) : base(x, y, width, height) { IsLayoutLocked = true; } /// /// Initializes a new instance of the class. /// public ContainerControl(Vector2 location, Vector2 size) : base(location, size) { IsLayoutLocked = true; } /// public ContainerControl(Rectangle bounds) : base(bounds) { IsLayoutLocked = true; } /// /// Gets child controls list /// public List Children => _children; /// /// Gets amount of the children controls /// public int ChildrenCount => _children.Count; /// /// Checks if container has any child controls /// public bool HasChildren => _children.Count > 0; /// /// Gets a value indicating whether the control, or one of its child controls, currently has the input focus. /// public override bool ContainsFocus => _containsFocus; /// /// True if automatic updates for control layout are locked (useful when creating a lot of GUI control to prevent lags). /// [HideInEditor, NoSerialize] public bool IsLayoutLocked { get; set; } /// /// Gets or sets a value indicating whether apply clipping mask on children during rendering. /// [EditorOrder(530), Tooltip("If checked, control will apply clipping mask on children during rendering.")] public bool ClipChildren { get; set; } = true; /// /// Gets or sets a value indicating whether perform view culling on children during rendering. /// [EditorOrder(540), Tooltip("If checked, control will perform view culling on children during rendering.")] public bool CullChildren { get; set; } = true; /// /// Locks all child controls layout and itself. /// [NoAnimate] public virtual void LockChildrenRecursive() { // Itself IsLayoutLocked = true; // Every child container control for (int i = 0; i < _children.Count; i++) { if (_children[i] is ContainerControl child) child.LockChildrenRecursive(); } } /// /// Unlocks all the child controls layout and itself. /// [NoAnimate] public virtual void UnlockChildrenRecursive() { // Itself IsLayoutLocked = false; // Every child container control for (int i = 0; i < _children.Count; i++) { if (_children[i] is ContainerControl child) child.UnlockChildrenRecursive(); } } /// /// Unlinks all the child controls. /// [NoAnimate] public virtual void RemoveChildren() { bool wasLayoutLocked = IsLayoutLocked; IsLayoutLocked = true; // Delete children while (_children.Count > 0) { _children[0].Parent = null; } IsLayoutLocked = wasLayoutLocked; PerformLayout(); } /// /// Removes and disposes all the child controls /// public virtual void DisposeChildren() { bool wasLayoutLocked = IsLayoutLocked; IsLayoutLocked = true; // Delete children while (_children.Count > 0) { _children[0].Dispose(); } IsLayoutLocked = wasLayoutLocked; PerformLayout(); } /// /// Creates a new control and adds it to the container. /// /// The added control. public T AddChild() where T : Control { var child = (T)Activator.CreateInstance(typeof(T)); child.Parent = this; return child; } /// /// Adds the control to the container. /// /// The control to add. /// The added control. public T AddChild(T child) where T : Control { if (child == null) throw new ArgumentNullException(); if (child.Parent == this && _children.Contains(child)) throw new InvalidOperationException("Argument child cannot be added, if current container is already its parent."); // Set child new parent child.Parent = this; return child; } /// /// Removes control from the container. /// /// The control to remove. public void RemoveChild(Control child) { if (child == null) throw new ArgumentNullException(); if (child.Parent != this) throw new InvalidOperationException("Argument child cannot be removed, if current container is not its parent."); // Unlink child.Parent = null; } /// /// Gets child control at given index. /// /// The control index. /// The child control. public Control GetChild(int index) { return _children[index]; } /// /// Searches for a child control of a specific type. If there are multiple controls matching the type, only the first one found is returned. /// /// The type of the control to search for. Includes any controls derived from the type. /// The control instance if found, otherwise null. public T GetChild() where T : Control { var type = typeof(T); for (int i = 0; i < _children.Count; i++) { var ct = _children[i].GetType(); if (type.IsAssignableFrom(ct)) return (T)_children[i]; } return null; } /// /// Gets zero-based index in the list of control children. /// /// The child control. /// The zero-based index in the list of control children. public int GetChildIndex(Control child) { return _children.IndexOf(child); } internal void ChangeChildIndex(Control child, int newIndex) { int oldIndex = _children.IndexOf(child); if (oldIndex == newIndex) return; _children.RemoveAt(oldIndex); // Check if index is invalid if (newIndex < 0 || newIndex >= _children.Count) { // Append at the end _children.Add(child); } else { // Change order _children.Insert(newIndex, child); } PerformLayout(); } /// /// Tries to find any child control at given point in control local coordinates. /// /// The local point to check. /// The found control index or -1 if failed. public int GetChildIndexAt(Vector2 point) { int result = -1; for (int i = _children.Count - 1; i >= 0; i--) { var child = _children[i]; // Check collision if (IntersectsChildContent(child, point, out var childLocation)) { result = i; break; } } return result; } /// /// Tries to find any child control at given point in control local coordinates /// /// The local point to check. /// The found control or null. public Control GetChildAt(Vector2 point) { Control result = null; for (int i = _children.Count - 1; i >= 0; i--) { var child = _children[i]; // Check collision if (IntersectsChildContent(child, point, out var childLocation)) { result = child; break; } } return result; } /// /// Tries to find valid child control at given point in control local coordinates. Uses custom callback method to test controls to pick. /// /// The local point to check. /// The control validation callback. /// The found control or null. public Control GetChildAt(Vector2 point, Func isValid) { if (isValid == null) throw new ArgumentNullException(nameof(isValid)); Control result = null; for (int i = _children.Count - 1; i >= 0; i--) { var child = _children[i]; // Check collision if (isValid(child) && IntersectsChildContent(child, point, out var childLocation)) { result = child; break; } } return result; } /// /// Tries to find lowest child control at given point in control local coordinates. /// /// The local point to check. /// The found control or null. public Control GetChildAtRecursive(Vector2 point) { Control result = null; for (int i = _children.Count - 1; i >= 0; i--) { var child = _children[i]; // Check collision if (IntersectsChildContent(child, point, out var childLocation)) { var containerControl = child as ContainerControl; var childAtRecursive = containerControl?.GetChildAtRecursive(childLocation); if (childAtRecursive != null) { child = childAtRecursive; } result = child; } } return result; } /// /// Gets rectangle in local control coordinates with area for controls (without scroll bars, anchored controls, etc.). /// /// The rectangle in local control coordinates with area for controls (without scroll bars etc.). public Rectangle GetClientArea() { GetDesireClientArea(out var clientArea); return clientArea; } /// /// Sort child controls list /// [NoAnimate] public void SortChildren() { _children.Sort(); PerformLayout(); } /// /// Sort children using recursion /// [NoAnimate] public void SortChildrenRecursive() { SortChildren(); for (int i = 0; i < _children.Count; i++) { var child = _children[i] as ContainerControl; child?.SortChildrenRecursive(); } } /// /// Called when child control gets resized. /// /// The resized control. public virtual void OnChildResized(Control control) { } /// /// Called when children collection gets changed (child added or removed). /// [NoAnimate] public virtual void OnChildrenChanged() { // Check if control isn't during disposing state if (!IsDisposing) { // Arrange child controls PerformLayout(); } } #region Internal Events /// internal override void CacheRootHandle() { base.CacheRootHandle(); for (int i = 0; i < _children.Count; i++) { _children[i].CacheRootHandle(); } } /// /// Adds a child control to the container. /// /// The control to add. internal virtual void AddChildInternal(Control child) { Assert.IsNotNull(child, "Invalid control."); // Add child _children.Add(child); OnChildrenChanged(); } /// /// Removes a child control from this container. /// /// The control to remove. internal virtual void RemoveChildInternal(Control child) { Assert.IsNotNull(child, "Invalid control."); // Remove child _children.Remove(child); OnChildrenChanged(); } /// /// Gets the desire client area rectangle for all the controls. /// /// The client area rectangle for child controls. public virtual void GetDesireClientArea(out Rectangle rect) { rect = new Rectangle(Vector2.Zero, Size); } /// /// Checks if given point in this container control space intersects with the child control content. /// Also calculates result location in child control space which can be used to feed control with event at that point. /// /// The child control to check. /// The location in this container control space. /// The output location in child control space. /// True if point is over the control content, otherwise false. public virtual bool IntersectsChildContent(Control child, Vector2 location, out Vector2 childSpaceLocation) { return child.IntersectsContent(ref location, out childSpaceLocation); } /// /// Update contain focus state and all it's children /// protected void UpdateContainsFocus() { // Get current state and update all children bool result = base.ContainsFocus; for (int i = 0; i < _children.Count; i++) { if (_children[i] is ContainerControl child) child.UpdateContainsFocus(); if (_children[i].ContainsFocus) result = true; } // Check if state has been changed if (result != _containsFocus) { // Cache flag _containsFocus = result; // Fire event if (result) { OnStartContainsFocus(); } else { OnEndContainsFocus(); } } } /// /// Updates child controls bounds. /// protected void UpdateChildrenBounds() { for (int i = 0; i < _children.Count; i++) { _children[i].UpdateBounds(); } } /// /// Perform layout for that container control before performing it for child controls. /// protected virtual void PerformLayoutBeforeChildren() { UpdateChildrenBounds(); } /// /// Perform layout for that container control after performing it for child controls. /// protected virtual void PerformLayoutAfterChildren() { } #endregion #region Control /// public override void OnDestroy() { // Steal focus from children if (ContainsFocus) { Focus(); } base.OnDestroy(); // Pass event further for (int i = 0; i < _children.Count; i++) { _children[i].OnDestroy(); } _children.Clear(); } /// public override bool IsMouseOver { get { if (base.IsMouseOver) return true; for (int i = 0; i < _children.Count && _children.Count > 0; i++) { if (_children[i].IsMouseOver) return true; } return false; } } /// public override bool IsTouchOver { get { if (base.IsTouchOver) return true; for (int i = 0; i < _children.Count && _children.Count > 0; i++) { if (_children[i].IsTouchOver) return true; } return false; } } /// public override void Update(float deltaTime) { base.Update(deltaTime); // Update all enabled child controls for (int i = 0; i < _children.Count; i++) { if (_children[i].Enabled) { _children[i].Update(deltaTime); } } } /// /// Draw the control and the children. /// public override void Draw() { DrawSelf(); if (ClipChildren) { GetDesireClientArea(out var clientArea); Render2D.PushClip(ref clientArea); DrawChildren(); Render2D.PopClip(); } else { DrawChildren(); } } /// /// Draws the control. /// public virtual void DrawSelf() { base.Draw(); } /// /// Draws the children. Can be overridden to provide some customizations. Draw is performed with applied clipping mask for the client area. /// protected virtual void DrawChildren() { // Draw all visible child controls if (CullChildren) { Render2D.PeekClip(out var globalClipping); Render2D.PeekTransform(out var globalTransform); for (int i = 0; i < _children.Count; i++) { var child = _children[i]; if (child.Visible) { Matrix3x3.Multiply(ref child._cachedTransform, ref globalTransform, out var globalChildTransform); var childGlobalRect = new Rectangle(globalChildTransform.M31, globalChildTransform.M32, child.Width * globalChildTransform.M11, child.Height * globalChildTransform.M22); if (globalClipping.Intersects(ref childGlobalRect)) { Render2D.PushTransform(ref child._cachedTransform); child.Draw(); Render2D.PopTransform(); } } } } else { for (int i = 0; i < _children.Count; i++) { var child = _children[i]; if (child.Visible) { Render2D.PushTransform(ref child._cachedTransform); child.Draw(); Render2D.PopTransform(); } } } } /// public override void PerformLayout(bool force = false) { if (IsLayoutLocked && !force) return; bool wasLocked = IsLayoutLocked; if (!wasLocked) LockChildrenRecursive(); PerformLayoutBeforeChildren(); for (int i = 0; i < _children.Count; i++) _children[i].PerformLayout(true); PerformLayoutAfterChildren(); if (!wasLocked) UnlockChildrenRecursive(); } /// public override void OnMouseEnter(Vector2 location) { // Check all children collisions with mouse and fire events for them for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = _children[i]; if (child.Visible && child.Enabled) { // Fire event if (IntersectsChildContent(child, location, out var childLocation)) { // Enter child.OnMouseEnter(childLocation); } } } base.OnMouseEnter(location); } /// public override void OnMouseMove(Vector2 location) { // Check all children collisions with mouse and fire events for them for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = _children[i]; if (child.Visible && child.Enabled) { // Fire events if (IntersectsChildContent(child, location, out var childLocation)) { if (child.IsMouseOver) { // Move child.OnMouseMove(childLocation); } else { // Enter child.OnMouseEnter(childLocation); } } else if (child.IsMouseOver) { // Leave child.OnMouseLeave(); } } } base.OnMouseMove(location); } /// public override void OnMouseLeave() { // Check all children collisions with mouse and fire events for them for (int i = 0; i < _children.Count && _children.Count > 0; i++) { var child = _children[i]; if (child.Visible && child.Enabled && child.IsMouseOver) { // Leave child.OnMouseLeave(); } } base.OnMouseLeave(); } /// public override bool OnMouseWheel(Vector2 location, float delta) { // Check all children collisions with mouse and fire events for them for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = _children[i]; if (child.Visible && child.Enabled) { // Fire events if (IntersectsChildContent(child, location, out var childLocation)) { // Wheel if (child.OnMouseWheel(childLocation, delta)) { return true; } } } } return base.OnMouseWheel(location, delta); } /// public override bool OnMouseDown(Vector2 location, MouseButton button) { // Check all children collisions with mouse and fire events for them for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = _children[i]; if (child.Visible && child.Enabled) { // Fire event if (IntersectsChildContent(child, location, out var childLocation)) { // Send event further if (child.OnMouseDown(childLocation, button)) { return true; } } } } return base.OnMouseDown(location, button); } /// public override bool OnMouseUp(Vector2 location, MouseButton button) { // Check all children collisions with mouse and fire events for them for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = _children[i]; if (child.Visible && child.Enabled) { // Fire event if (IntersectsChildContent(child, location, out var childLocation)) { // Send event further if (child.OnMouseUp(childLocation, button)) { return true; } } } } return base.OnMouseUp(location, button); } /// public override bool OnMouseDoubleClick(Vector2 location, MouseButton button) { // Check all children collisions with mouse and fire events for them for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = _children[i]; if (child.Visible && child.Enabled) { // Fire event if (IntersectsChildContent(child, location, out var childLocation)) { // Send event further if (child.OnMouseDoubleClick(childLocation, button)) { return true; } } } } return base.OnMouseDoubleClick(location, button); } /// public override bool IsTouchPointerOver(int pointerId) { if (base.IsTouchPointerOver(pointerId)) return true; for (int i = 0; i < _children.Count && _children.Count > 0; i++) { if (_children[i].IsTouchPointerOver(pointerId)) return true; } return false; } /// public override void OnTouchEnter(Vector2 location, int pointerId) { for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = _children[i]; if (child.Visible && child.Enabled && !child.IsTouchPointerOver(pointerId)) { if (IntersectsChildContent(child, location, out var childLocation)) { child.OnTouchEnter(childLocation, pointerId); } } } base.OnTouchEnter(location, pointerId); } /// public override bool OnTouchDown(Vector2 location, int pointerId) { for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = _children[i]; if (child.Visible && child.Enabled) { if (IntersectsChildContent(child, location, out var childLocation)) { if (!child.IsTouchPointerOver(pointerId)) { child.OnTouchEnter(location, pointerId); } if (child.OnTouchDown(childLocation, pointerId)) { return true; } } } } return base.OnTouchDown(location, pointerId); } /// public override void OnTouchMove(Vector2 location, int pointerId) { for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = _children[i]; if (child.Visible && child.Enabled) { if (IntersectsChildContent(child, location, out var childLocation)) { if (child.IsTouchPointerOver(pointerId)) { child.OnTouchMove(childLocation, pointerId); } else { child.OnTouchEnter(childLocation, pointerId); } } else if (child.IsTouchPointerOver(pointerId)) { child.OnTouchLeave(pointerId); } } } base.OnTouchMove(location, pointerId); } /// public override bool OnTouchUp(Vector2 location, int pointerId) { for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = _children[i]; if (child.Visible && child.Enabled && child.IsTouchPointerOver(pointerId)) { if (IntersectsChildContent(child, location, out var childLocation)) { if (child.OnTouchUp(childLocation, pointerId)) { return true; } } } } return base.OnTouchUp(location, pointerId); } /// public override void OnTouchLeave(int pointerId) { for (int i = 0; i < _children.Count && _children.Count > 0; i++) { var child = _children[i]; if (child.Visible && child.Enabled && child.IsTouchPointerOver(pointerId)) { child.OnTouchLeave(pointerId); } } base.OnTouchLeave(pointerId); } /// public override bool OnCharInput(char c) { for (int i = 0; i < _children.Count && _children.Count > 0; i++) { var child = _children[i]; if (child.Enabled && child.ContainsFocus) { return child.OnCharInput(c); } } return false; } /// public override bool OnKeyDown(KeyboardKeys key) { for (int i = 0; i < _children.Count && _children.Count > 0; i++) { var child = _children[i]; if (child.Enabled && child.ContainsFocus) { return child.OnKeyDown(key); } } return false; } /// public override void OnKeyUp(KeyboardKeys key) { for (int i = 0; i < _children.Count && _children.Count > 0; i++) { var child = _children[i]; if (child.Enabled && child.ContainsFocus) { child.OnKeyUp(key); break; } } } /// public override DragDropEffect OnDragEnter(ref Vector2 location, DragData data) { // Base var result = base.OnDragEnter(ref location, data); // Check all children collisions with mouse and fire events for them for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = _children[i]; if (child.Visible && child.Enabled) { // Fire event if (IntersectsChildContent(child, location, out var childLocation)) { // Enter result = child.OnDragEnter(ref childLocation, data); if (result != DragDropEffect.None) break; } } } return result; } /// public override DragDropEffect OnDragMove(ref Vector2 location, DragData data) { // Base var result = base.OnDragMove(ref location, data); // Check all children collisions with mouse and fire events for them for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = _children[i]; if (child.Visible && child.Enabled) { // Fire events if (IntersectsChildContent(child, location, out var childLocation)) { if (child.IsDragOver) { // Move var tmpResult = child.OnDragMove(ref childLocation, data); if (tmpResult != DragDropEffect.None) result = tmpResult; } else { // Enter var tmpResult = child.OnDragEnter(ref childLocation, data); if (tmpResult != DragDropEffect.None) result = tmpResult; } } else if (child.IsDragOver) { // Leave child.OnDragLeave(); } } } return result; } /// public override void OnDragLeave() { // Base base.OnDragLeave(); // Check all children collisions with mouse and fire events for them for (int i = 0; i < _children.Count && _children.Count > 0; i++) { var child = _children[i]; if (child.IsDragOver) { // Leave child.OnDragLeave(); } } } /// public override DragDropEffect OnDragDrop(ref Vector2 location, DragData data) { // Base var result = base.OnDragDrop(ref location, data); // Check all children collisions with mouse and fire events for them for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = _children[i]; if (child.Visible && child.Enabled) { // Fire event if (IntersectsChildContent(child, location, out var childLocation)) { // Enter result = child.OnDragDrop(ref childLocation, data); if (result != DragDropEffect.None) break; } } } return result; } /// protected override void OnSizeChanged() { // Lock updates to prevent additional layout calculations bool wasLayoutLocked = IsLayoutLocked; IsLayoutLocked = true; // Base base.OnSizeChanged(); // Fire event for (int i = 0; i < _children.Count; i++) { _children[i].OnParentResized(); } // Restore state IsLayoutLocked = wasLayoutLocked; // Arrange child controls PerformLayout(); } #endregion } }