// Copyright (c) Wojciech Figat. All rights reserved. using System; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.GUI.Tree { /// /// Tree node control. /// /// [HideInEditor] public class TreeNode : ContainerControl { /// /// The default drag insert position margin. /// public const float DefaultDragInsertPositionMargin = 3.0f; /// /// The default node offset on Y axis. /// public const float DefaultNodeOffsetY = 0; private const float _targetHighlightScale = 1.25f; private const float _highlightScaleAnimDuration = 0.85f; private Tree _tree; private bool _opened, _canChangeOrder; private float _animationProgress, _cachedHeight; private bool _isHightlighted; private float _targetHighlightTimeSec; private float _currentHighlightTimeSec; // Used to prevent showing highlight on double mouse click private float _debounceHighlightTime; private float _highlightScale; private bool _mouseOverArrow, _mouseOverHeader; private float _xOffset, _textWidth; private float _headerHeight = 16.0f; private Rectangle _headerRect; private SpriteHandle _iconCollaped, _iconOpened; private Margin _margin = new Margin(2.0f); private string _text; private bool _textChanged; private bool _isMouseDown; private float _mouseDownTime; private Float2 _mouseDownPos; private DragItemPositioning _dragOverMode; private bool _isDragOverHeader; private static ulong _dragEndFrame; /// /// Gets or sets the text. /// [EditorOrder(10), Tooltip("The node text.")] public string Text { get => _text; set { _text = value; _textChanged = true; PerformLayout(); } } /// /// Gets or sets a value indicating whether this node is expanded. /// [EditorOrder(20), Tooltip("If checked, node is expanded.")] public bool IsExpanded { get => _opened; set { if (value) Expand(true); else Collapse(true); } } /// /// Gets or sets a value indicating whether this node is collapsed. /// [HideInEditor, NoSerialize] public bool IsCollapsed { get => !_opened; set { if (value) Collapse(true); else Expand(true); } } /// /// Gets a value indicating whether the node is collapsed in the hierarchy (is collapsed or any of its parents is collapsed). /// public bool IsCollapsedInHierarchy => IsCollapsed || (Parent is TreeNode parentNode && parentNode.IsCollapsedInHierarchy); /// /// Gets or sets the text margin. /// [EditorOrder(30), Tooltip("The margin of the text area.")] public Margin TextMargin { get => _margin; set { _margin = value; PerformLayout(); } } /// /// Gets or sets the color of the text. /// [EditorDisplay("Style"), EditorOrder(2000)] public Color TextColor { get; set; } /// /// Gets or sets the font used to render text. /// [EditorDisplay("Style"), EditorOrder(2000)] public FontReference TextFont { get; set; } /// /// Gets or sets the color of the background when tree node is selected. /// [EditorDisplay("Style"), EditorOrder(2000)] public Color BackgroundColorSelected { get; set; } /// /// Gets or sets the color of the background when tree node is highlighted. /// [EditorDisplay("Style"), EditorOrder(2000)] public Color BackgroundColorHighlighted { get; set; } /// /// Gets or sets the color of the background when tree node is selected but not focused. /// [EditorDisplay("Style"), EditorOrder(2000)] public Color BackgroundColorSelectedUnfocused { get; set; } /// /// Gets the parent tree control. /// public Tree ParentTree { get { if (_tree == null) { if (Parent is TreeNode upNode) _tree = upNode.ParentTree; else if (Parent is Tree tree) _tree = tree; } return _tree; } } /// /// Gets a value indicating whether this node is root. /// public bool IsRoot => !(Parent is TreeNode); /// /// Gets the minimum width of the node sub-tree. /// public virtual float MinimumWidth { get { UpdateTextWidth(); float minWidth = _xOffset + _textWidth + 6 + 16; if (_iconCollaped.IsValid) minWidth += 16; if (_opened || _animationProgress < 1.0f) { for (int i = 0; i < _children.Count; i++) { if (_children[i] is TreeNode node && node.Visible) { minWidth = Mathf.Max(minWidth, node.MinimumWidth); } } } return minWidth; } } /// /// The indent applied to the child nodes. /// [EditorOrder(30), Tooltip("The indentation applied to the child nodes.")] public float ChildrenIndent { get; set; } = 12.0f; /// /// The height of the tree node header area. /// [EditorOrder(40), Limit(1, 10000, 0.1f), Tooltip("The height of the tree node header area.")] public float HeaderHeight { get => _headerHeight; set { if (!Mathf.NearEqual(_headerHeight, value)) { _headerHeight = value; PerformLayout(); } } } /// /// Gets or sets the color of the icon. /// [EditorOrder(50), Tooltip("The color of the icon.")] public Color IconColor { get; set; } = Color.White; /// /// Gets the arrow rectangle. /// public Rectangle ArrowRect => CustomArrowRect.HasValue ? CustomArrowRect.Value : new Rectangle(_xOffset + 2 + _margin.Left, 2, 12, 12); /// /// Gets the header rectangle. /// public Rectangle HeaderRect => _headerRect; /// /// Gets the header text rectangle. /// public Rectangle TextRect { get { var left = _xOffset + 16; // offset + arrow var textRect = new Rectangle(left, 0, Width - left, _headerHeight); // Margin _margin.ShrinkRectangle(ref textRect); // Icon if (_iconCollaped.IsValid) { textRect.X += 18.0f; textRect.Width -= 18.0f; } return textRect; } } /// /// Custom arrow rectangle within node. /// [HideInEditor, NoSerialize] public Rectangle? CustomArrowRect; /// /// Gets the drag over action type. /// public DragItemPositioning DragOverMode => _dragOverMode; /// /// Gets a value indicating whether this node has any visible child. Returns false if it has no children. /// public bool HasAnyVisibleChild { get { bool result = false; for (int i = 0; i < _children.Count; i++) { if (_children[i] is TreeNode node && node.Visible) { result = true; break; } } return result; } } /// /// Initializes a new instance of the class. /// public TreeNode() : this(false, SpriteHandle.Invalid, SpriteHandle.Invalid) { } /// /// Initializes a new instance of the class. /// /// Enable/disable changing node order in parent tree node. public TreeNode(bool canChangeOrder) : this(canChangeOrder, SpriteHandle.Invalid, SpriteHandle.Invalid) { } /// /// Initializes a new instance of the class. /// /// Enable/disable changing node order in parent tree node. /// The icon for node collapsed. /// The icon for node opened. public TreeNode(bool canChangeOrder, SpriteHandle iconCollapsed, SpriteHandle iconOpened) : base(0, 0, 64, 16) { AutoFocus = true; _canChangeOrder = canChangeOrder; _animationProgress = 1.0f; _cachedHeight = _headerHeight; _iconCollaped = iconCollapsed; _iconOpened = iconOpened; _mouseDownTime = -1; var style = Style.Current; TextColor = style.Foreground; BackgroundColorSelected = style.BackgroundSelected; BackgroundColorHighlighted = style.BackgroundHighlighted; BackgroundColorSelectedUnfocused = style.LightBackground; TextFont = new FontReference(style.FontSmall); } /// /// Expand node. /// /// True if skip node expanding animation. public void Expand(bool noAnimation = false) { // Parents first ExpandAllParents(noAnimation); // Change state if (_opened && _animationProgress >= 1.0f) return; bool prevState = _opened; _opened = true; if (noAnimation) _animationProgress = 1.0f; else if (prevState != _opened) _animationProgress = 1.0f - _animationProgress; // Update OnExpandedChanged(); OnExpandAnimationChanged(); } /// /// Collapse node. /// /// True if skip node expanding animation. public void Collapse(bool noAnimation = false) { // Change state if (!_opened && _animationProgress >= 1.0f) return; bool prevState = _opened; _opened = false; if (noAnimation) _animationProgress = 1.0f; else if (prevState != _opened) _animationProgress = 1.0f - _animationProgress; // Update OnExpandedChanged(); OnExpandAnimationChanged(); } /// /// Expand node and all the children. /// /// True if skip node expanding animation. public void ExpandAll(bool noAnimation = false) { bool wasLayoutLocked = IsLayoutLocked; IsLayoutLocked = true; Expand(noAnimation); for (int i = 0; i < _children.Count; i++) { if (_children[i] is TreeNode node) { node.ExpandAll(noAnimation); } } IsLayoutLocked = wasLayoutLocked; PerformLayout(); } /// /// Collapse node and all the children. /// /// True if skip node expanding animation. public void CollapseAll(bool noAnimation = false) { bool wasLayoutLocked = IsLayoutLocked; IsLayoutLocked = true; Collapse(noAnimation); for (int i = 0; i < _children.Count; i++) { if (_children[i] is TreeNode node) { node.CollapseAll(noAnimation); } } IsLayoutLocked = wasLayoutLocked; PerformLayout(); } /// /// Ensure that all node parents are expanded. /// /// True if skip node expanding animation. public void ExpandAllParents(bool noAnimation = false) { (Parent as TreeNode)?.Expand(noAnimation); } /// /// Ends open/close animation by force. /// public void EndAnimation() { if (_animationProgress < 1.0f) { _animationProgress = 1.0f; OnExpandAnimationChanged(); } } /// /// Select node in the tree. /// public void Select() { ParentTree.Select(this); } /// /// Called when drag and drop enters the node header area. /// /// The data. /// Drag action response. protected virtual DragDropEffect OnDragEnterHeader(DragData data) { return DragDropEffect.None; } /// /// Called when drag and drop moves over the node header area. /// /// The data. /// Drag action response. protected virtual DragDropEffect OnDragMoveHeader(DragData data) { return DragDropEffect.None; } /// /// Called when drag and drop performs over the node header area. /// /// The data. /// Drag action response. protected virtual DragDropEffect OnDragDropHeader(DragData data) { return DragDropEffect.None; } /// /// Called when drag and drop leaves the node header area. /// protected virtual void OnDragLeaveHeader() { } /// /// Begins the drag drop operation. /// protected virtual void DoDragDrop() { } /// /// Called when mouse double clicks header. /// /// The mouse location. /// The button. /// True if event has been handled. protected virtual bool OnMouseDoubleClickHeader(ref Float2 location, MouseButton button) { if (HasAnyVisibleChild) { // Toggle open state if (_opened) Collapse(); else Expand(); } // Handled return true; } /// /// Called when mouse is pressing node header for a long time. /// protected virtual void OnLongPress() { } /// /// Called when expanded/collapsed state changes. /// protected virtual void OnExpandedChanged() { } /// /// Called when expand/collapse animation progress changes. /// protected virtual void OnExpandAnimationChanged() { if (ParentTree != null) ParentTree.PerformLayout(); else if (Parent != null) Parent.PerformLayout(); else PerformLayout(); } /// /// Tests the header hit. /// /// The location. /// True if hits it. protected virtual bool TestHeaderHit(ref Float2 location) { return _headerRect.Contains(location); } /// /// Updates the drag over mode based on the given mouse location. /// /// The location. private void UpdateDragPositioning(ref Float2 location) { // Check collision with drag areas if (new Rectangle(_headerRect.X, _headerRect.Y - DefaultDragInsertPositionMargin - DefaultNodeOffsetY, _headerRect.Width, DefaultDragInsertPositionMargin * 2.0f).Contains(location)) _dragOverMode = DragItemPositioning.Above; else if ((IsCollapsed || !HasAnyVisibleChild) && new Rectangle(_headerRect.X, _headerRect.Bottom - DefaultDragInsertPositionMargin, _headerRect.Width, DefaultDragInsertPositionMargin * 2.0f).Contains(location)) _dragOverMode = DragItemPositioning.Below; else _dragOverMode = DragItemPositioning.At; // Update DraggedOverNode var tree = ParentTree; if (_dragOverMode == DragItemPositioning.None) { if (tree != null && tree.DraggedOverNode == this) tree.DraggedOverNode = null; } else if (tree != null) tree.DraggedOverNode = this; } private void ClearDragPositioning() { _dragOverMode = DragItemPositioning.None; var tree = ParentTree; if (tree != null && tree.DraggedOverNode == this) tree.DraggedOverNode = null; } /// /// Caches the color of the text for this node. Called during update before children nodes but after parent node so it can reuse parent tree node data. /// /// Text color. protected virtual Color CacheTextColor() { return Enabled ? TextColor : TextColor * 0.6f; } /// /// Updates the cached width of the text. /// protected void UpdateTextWidth() { if (_textChanged) { var font = TextFont.GetFont(); if (font) { _textWidth = font.MeasureText(_text).X; _textChanged = false; } } } /// /// Adds a box around the text to highlight the node. /// /// The duration of the highlight in seconds. public void StartHighlight(float durationSec = 3) { _isHightlighted = true; _targetHighlightTimeSec = durationSec; _currentHighlightTimeSec = 0; _debounceHighlightTime = 0; _highlightScale = 2f; } /// /// Stops any current highlight. /// public void StopHighlight() { _isHightlighted = false; _targetHighlightTimeSec = 0; _currentHighlightTimeSec = 0; _debounceHighlightTime = 0; } /// public override void Update(float deltaTime) { // Highlight animations if (_isHightlighted) { _debounceHighlightTime += deltaTime; _currentHighlightTimeSec += deltaTime; // In the first second, animate the highlight to shrink into it's resting position if (_currentHighlightTimeSec < _highlightScaleAnimDuration) _highlightScale = Mathf.Lerp(_highlightScale, _targetHighlightScale, _currentHighlightTimeSec); if (_currentHighlightTimeSec >= _targetHighlightTimeSec) _isHightlighted = false; } // Drop/down animation if (_animationProgress < 1.0f) { bool isDeltaSlow = deltaTime > (1 / 20.0f); // Update progress if (isDeltaSlow) { _animationProgress = 1.0f; } else { const float openCloseAnimationTime = 0.1f; _animationProgress += deltaTime / openCloseAnimationTime; if (_animationProgress > 1.0f) _animationProgress = 1.0f; } // Arrange controls OnExpandAnimationChanged(); } // Check for long press const float longPressTimeSeconds = 0.6f; if (_isMouseDown && Time.UnscaledGameTime - _mouseDownTime > longPressTimeSeconds) { OnLongPress(); } // Don't update collapsed children if (_opened) { base.Update(deltaTime); } } /// public override void Draw() { // Cache data var style = Style.Current; var tree = ParentTree; bool isSelected = tree.Selection.Contains(this); bool isFocused = tree.ContainsFocus; var left = _xOffset + 16; // offset + arrow var textRect = new Rectangle(left, 0, Width - left, _headerHeight); _margin.ShrinkRectangle(ref textRect); // Draw background if (isSelected || _mouseOverHeader) { Render2D.FillRectangle(_headerRect, (isSelected && isFocused) ? BackgroundColorSelected : (_mouseOverHeader ? BackgroundColorHighlighted : BackgroundColorSelectedUnfocused)); } // Draw arrow if (HasAnyVisibleChild) { Render2D.DrawSprite(_opened ? style.ArrowDown : style.ArrowRight, ArrowRect, _mouseOverHeader ? style.Foreground : style.ForegroundGrey); } // Draw icon if (_iconCollaped.IsValid) { Render2D.DrawSprite(_opened ? _iconOpened : _iconCollaped, new Rectangle(textRect.Left, 0, 16, 16), IconColor); textRect.X += 18.0f; textRect.Width -= 18.0f; } float textWidth = TextFont.GetFont().MeasureText(_text).X; Rectangle trueTextRect = textRect; trueTextRect.Width = textWidth; trueTextRect.Scale(_highlightScale); if (_isHightlighted && _debounceHighlightTime > 0.1f) { Color highlightBackgroundColor = Editor.Instance.Options.Options.Visual.HighlightColor; highlightBackgroundColor = highlightBackgroundColor.AlphaMultiplied(0.3f); Render2D.FillRectangle(trueTextRect, highlightBackgroundColor); } // Draw text Color textColor = CacheTextColor(); Render2D.DrawText(TextFont.GetFont(), _text, textRect, textColor, TextAlignment.Near, TextAlignment.Center); // Draw drag and drop effect if (IsDragOver && _tree.DraggedOverNode == this) { switch (_dragOverMode) { case DragItemPositioning.At: Render2D.FillRectangle(textRect, style.Selection); Render2D.DrawRectangle(textRect, style.SelectionBorder); break; case DragItemPositioning.Above: Render2D.DrawRectangle(new Rectangle(textRect.X, textRect.Top - DefaultDragInsertPositionMargin * 0.5f - DefaultNodeOffsetY - _margin.Top, textRect.Width, DefaultDragInsertPositionMargin), style.SelectionBorder); break; case DragItemPositioning.Below: Render2D.DrawRectangle(new Rectangle(textRect.X, textRect.Bottom + _margin.Bottom - DefaultDragInsertPositionMargin * 0.5f, textRect.Width, DefaultDragInsertPositionMargin), style.SelectionBorder); break; } } // Show tree guidelines if (Editor.Instance.Options.Options.Interface.ShowTreeLines) { ContainerControl parent = Parent; TreeNode parentNode = parent as TreeNode; bool thisNodeIsLast = false; while (parentNode != null && (parentNode != tree.Children[0] || tree.DrawRootTreeLine)) { float bottomOffset = 0; float topOffset = 0; if (parent == parentNode && this == parent.Children[0]) topOffset = 2; if (thisNodeIsLast && parentNode.Children.Count == 1) bottomOffset = topOffset != 0 ? 4 : 2; if (parent == parentNode && this == parent.Children[^1] && !_opened) { thisNodeIsLast = true; bottomOffset = topOffset != 0 ? 4 : 2; } float leftOffset = 9; // Adjust offset for icon image if (_iconCollaped.IsValid) leftOffset += 18; var lineRect1 = new Rectangle(parentNode.TextRect.Left - leftOffset, parentNode.HeaderRect.Top + topOffset, 1, parentNode.HeaderRect.Height - bottomOffset); if (HasAnyVisibleChild && CustomArrowRect.HasValue && CustomArrowRect.Value.Intersects(lineRect1)) lineRect1 = Rectangle.Empty; // Skip drawing line if it's overlapping the arrow rectangle Render2D.FillRectangle(lineRect1, isSelected ? style.ForegroundGrey : style.LightBackground); parentNode = parentNode.Parent as TreeNode; } } if (_isHightlighted && _debounceHighlightTime > 0.1f) { // Draw highlights Render2D.DrawRectangle(trueTextRect, Editor.Instance.Options.Options.Visual.HighlightColor, 3); } // Base if (_opened) { if (ClipChildren) { Render2D.PushClip(new Rectangle(0, _headerHeight, Width, Height - _headerHeight)); base.Draw(); Render2D.PopClip(); } else { base.Draw(); } } } /// protected override void DrawChildren() { // Draw all visible child controls var children = _children; if (children.Count == 0) return; var last = children.Count - 1; if (CullChildren) { Render2D.PeekClip(out var globalClipping); Render2D.PeekTransform(out var globalTransform); // Try to estimate the rough location of the first and the last nodes, assuming the node height is constant var firstChildGlobalRect = GetChildGlobalRectangle(children[0], ref globalTransform); var firstVisibleChild = Math.Clamp((int)Math.Floor((globalClipping.Top - firstChildGlobalRect.Top) / _headerHeight) + 1, 0, last); if (GetChildGlobalRectangle(children[firstVisibleChild], ref globalTransform).Top > globalClipping.Top || !children[firstVisibleChild].Visible) { // Estimate overshoot, either it's partially visible or hidden in the tree for (; firstVisibleChild > 0; firstVisibleChild--) { var child = children[firstVisibleChild]; if (!child.Visible) continue; if (GetChildGlobalRectangle(child, ref globalTransform).Top < globalClipping.Top) break; } } var lastVisibleChild = Math.Clamp((int)Math.Ceiling((globalClipping.Bottom - firstChildGlobalRect.Top) / _headerHeight) + 1, firstVisibleChild, last); if (GetChildGlobalRectangle(children[lastVisibleChild], ref globalTransform).Top < globalClipping.Bottom || !children[lastVisibleChild].Visible) { // Estimate overshoot, either it's partially visible or hidden in the tree for (; lastVisibleChild < last; lastVisibleChild++) { var child = children[lastVisibleChild]; if (!child.Visible) continue; if (GetChildGlobalRectangle(child, ref globalTransform).Top > globalClipping.Bottom) break; } } for (int i = firstVisibleChild; i <= lastVisibleChild; i++) { var child = children[i]; if (!child.Visible) continue; Render2D.PushTransform(child._cachedTransform); child.Draw(); Render2D.PopTransform(); } static Rectangle GetChildGlobalRectangle(Control control, ref Matrix3x3 globalTransform) { Matrix3x3.Multiply(control._cachedTransform, globalTransform, out var globalChildTransform); return new Rectangle(globalChildTransform.M31, globalChildTransform.M32, control.Width * globalChildTransform.M11, control.Height * globalChildTransform.M22); } } else { for (int i = 0; i <= last; i++) { var child = children[i]; if (child.Visible) { Render2D.PushTransform(child._cachedTransform); child.Draw(); Render2D.PopTransform(); } } } } /// public override bool OnMouseDown(Float2 location, MouseButton button) { UpdateMouseOverFlags(location); // Check if mouse hits bar and node isn't a root if (_mouseOverHeader) { // Check if left button goes down if (button == MouseButton.Left) { _isMouseDown = true; _mouseDownPos = location; _mouseDownTime = Time.UnscaledGameTime; } // Handled Focus(); return true; } // Base if (_opened) return base.OnMouseDown(location, button); // Handled Focus(); return true; } /// public override bool OnMouseUp(Float2 location, MouseButton button) { UpdateMouseOverFlags(location); // Clear flag for left button if (button == MouseButton.Left && _isMouseDown) { _isMouseDown = false; _mouseDownTime = -1; } // Check if mouse hits bar and node isn't a root if (_mouseOverHeader) { // Skip mouse up event right after drag drop ends if (button == MouseButton.Left && Engine.FrameCount - _dragEndFrame < 10) return true; // Prevent from selecting node when user is just clicking at an arrow if (!_mouseOverArrow) { // Check if user is pressing control key var tree = ParentTree; var window = tree.Root; if (window.GetKey(KeyboardKeys.Shift)) { // Select range tree.SelectRange(this); } else if (window.GetKey(KeyboardKeys.Control)) { // Add/Remove tree.AddOrRemoveSelection(this); } else if (button == MouseButton.Right && tree.Selection.Contains(this)) { // Do nothing } else { // Select tree.Select(this); } } // Check if mouse hits arrow if (_mouseOverArrow && HasAnyVisibleChild) { if (ParentTree.Root.GetKey(KeyboardKeys.Alt)) { if (_opened) CollapseAll(); else ExpandAll(); } else { if (_opened) Collapse(); else Expand(); } } // Check if mouse hits bar if (button == MouseButton.Right && TestHeaderHit(ref location)) { ParentTree.OnRightClickInternal(this, ref location); } // Handled Focus(); return true; } // Check if mouse hits bar if (button == MouseButton.Right && TestHeaderHit(ref location)) { ParentTree.OnRightClickInternal(this, ref location); } // Base return base.OnMouseUp(location, button); } /// public override bool OnMouseDoubleClick(Float2 location, MouseButton button) { // Check if mouse hits bar if (TestHeaderHit(ref location)) { return OnMouseDoubleClickHeader(ref location, button); } // Check if animation has been finished if (_animationProgress >= 1.0f) { // Base return base.OnMouseDoubleClick(location, button); } return false; } /// public override void OnMouseMove(Float2 location) { UpdateMouseOverFlags(location); // Check if start drag and drop if (_isMouseDown && Float2.Distance(_mouseDownPos, location) > 10.0f) { // Clear flag _isMouseDown = false; _mouseDownTime = -1; // Start DoDragDrop(); return; } // Check if animation has been finished if (_animationProgress >= 1.0f) { // Base if (_opened) base.OnMouseMove(location); } } private void UpdateMouseOverFlags(Vector2 location) { // Cache flags _mouseOverArrow = HasAnyVisibleChild && ArrowRect.Contains(location); _mouseOverHeader = new Rectangle(0, 0, Width, _headerHeight - 1).Contains(location); if (_mouseOverHeader) { // Allow non-scrollable controls to stay on top of the header and override the mouse behaviour for (int i = 0; i < Children.Count; i++) { if (!Children[i].IsScrollable && IntersectsChildContent(Children[i], location, out _)) { _mouseOverHeader = false; break; } } } } /// public override void OnMouseLeave() { // Clear flags _mouseOverArrow = false; _mouseOverHeader = false; // Check if start drag and drop if (_isMouseDown) { // Clear flag _isMouseDown = false; _mouseDownTime = -1; // Start DoDragDrop(); } // Base base.OnMouseLeave(); } /// public override bool OnKeyDown(KeyboardKeys key) { // Base if (_opened) return base.OnKeyDown(key); return false; } /// public override void OnKeyUp(KeyboardKeys key) { // Base if (_opened) base.OnKeyUp(key); } /// public override void OnChildResized(Control control) { // Optimize if child is tree node that is not visible if (!_opened && control is TreeNode) return; PerformLayout(); base.OnChildResized(control); } /// public override DragDropEffect OnDragEnter(ref Float2 location, DragData data) { var result = base.OnDragEnter(ref location, data); // Check if no children handled that event _dragOverMode = DragItemPositioning.None; if (result == DragDropEffect.None) { UpdateDragPositioning(ref location); // Check if mouse is over header _isDragOverHeader = TestHeaderHit(ref location); if (_isDragOverHeader) { if (ParentTree != null) ParentTree.DraggedOverNode = this; // Expand node if mouse goes over arrow if (ArrowRect.Contains(location) && HasAnyVisibleChild) Expand(true); result = OnDragEnterHeader(data); } if (result == DragDropEffect.None) _dragOverMode = DragItemPositioning.None; } return result; } /// public override DragDropEffect OnDragMove(ref Float2 location, DragData data) { var result = base.OnDragMove(ref location, data); // Check if no children handled that event ClearDragPositioning(); if (result == DragDropEffect.None) { UpdateDragPositioning(ref location); // Check if mouse is over header bool isDragOverHeader = TestHeaderHit(ref location); if (isDragOverHeader) { if (ParentTree != null) ParentTree.DraggedOverNode = this; // Expand node if mouse goes over arrow if (ArrowRect.Contains(location) && HasAnyVisibleChild) Expand(true); if (!_isDragOverHeader) result = OnDragEnterHeader(data); else result = OnDragMoveHeader(data); } else if (_isDragOverHeader) { OnDragLeaveHeader(); } _isDragOverHeader = isDragOverHeader; if (result == DragDropEffect.None) _dragOverMode = DragItemPositioning.None; } return result; } /// public override DragDropEffect OnDragDrop(ref Float2 location, DragData data) { var result = base.OnDragDrop(ref location, data); // Check if no children handled that event if (result == DragDropEffect.None) { UpdateDragPositioning(ref location); _dragEndFrame = Engine.FrameCount; // Check if mouse is over header if (TestHeaderHit(ref location)) { result = OnDragDropHeader(data); } } // Clear cache _isDragOverHeader = false; ClearDragPositioning(); return result; } /// public override void OnDragLeave() { // Clear cache if (_isDragOverHeader) { _isDragOverHeader = false; OnDragLeaveHeader(); } ClearDragPositioning(); base.OnDragLeave(); } /// public override bool OnTestTooltipOverControl(ref Float2 location) { return TestHeaderHit(ref location) && ShowTooltip; } /// public override bool OnShowTooltip(out string text, out Float2 location, out Rectangle area) { text = TooltipText; location = _headerRect.Size * new Float2(0.5f, 1.0f); area = new Rectangle(Float2.Zero, _headerRect.Size); return ShowTooltip; } /// protected override void OnSizeChanged() { base.OnSizeChanged(); _headerRect = new Rectangle(0, 0, Width, _headerHeight); } /// public override void PerformLayout(bool force = false) { if (_isLayoutLocked && !force) return; bool wasLocked = _isLayoutLocked; if (!wasLocked) LockChildrenRecursive(); // Auto-size tree nodes to match the parent size var parent = Parent; var width = parent is TreeNode ? parent.Width : Width; // Optimize layout logic if node is collapsed if (_opened || _animationProgress < 1.0f) { Width = width; PerformLayoutBeforeChildren(); for (int i = 0; i < _children.Count; i++) _children[i].PerformLayout(true); PerformLayoutAfterChildren(); } else { // TODO: perform layout for any non-TreeNode controls _cachedHeight = _headerHeight; Size = new Float2(width, _headerHeight); } if (!wasLocked) UnlockChildrenRecursive(); } /// protected override void PerformLayoutBeforeChildren() { if (_opened) { // Update the nodes nesting level before the actual positioning float xOffset = _xOffset + ChildrenIndent; for (int i = 0; i < _children.Count; i++) { if (_children[i] is TreeNode node) node._xOffset = xOffset; } } base.PerformLayoutBeforeChildren(); } /// protected override void PerformLayoutAfterChildren() { float y = _headerHeight; float height = _headerHeight; float xOffset = _xOffset + ChildrenIndent; // Skip full layout if it's fully collapsed if (_opened || _animationProgress < 1.0f) { y -= _cachedHeight * (_opened ? 1.0f - _animationProgress : _animationProgress); for (int i = 0; i < _children.Count; i++) { if (_children[i] is TreeNode node && node.Visible) { node._xOffset = xOffset; node.Location = new Float2(0, y); float nodeHeight = node.Height + DefaultNodeOffsetY; y += nodeHeight; height += nodeHeight; } } } _cachedHeight = height; Height = Mathf.Max(_headerHeight, y); } /// protected override bool CanNavigateChild(Control child) { // Closed tree node skips navigation for hidden children if (IsCollapsed && child is TreeNode) return false; return base.CanNavigateChild(child); } /// protected override void OnParentChangedInternal() { _tree = null; base.OnParentChangedInternal(); } /// public override int Compare(Control other) { if (other is TreeNode node) { return string.Compare(Text, node.Text, StringComparison.InvariantCulture); } return base.Compare(other); } /// public override void OnDestroy() { ParentTree?.Selection.Remove(this); base.OnDestroy(); } } }