// Copyright (c) Wojciech Figat. All rights reserved. using System; namespace FlaxEngine.GUI { /// /// Radial menu control that arranges child controls (of type Image) in a circle. /// /// public class RadialMenu : ContainerControl { private bool _materialIsDirty = true; private float _angle; private float _selectedSegment; private int _highlightSegment = -1; private MaterialBase _material; private MaterialInstance _materialInstance; private int _segmentCount; private Color _highlightColor; private Color _foregroundColor; private Color _selectionColor; private float _edgeOffset; private float _thickness = 0.2f; private float USize => Size.X < Size.Y ? Size.X : Size.Y; private bool ShowMatProp => _material != null; /// /// The material to use for menu background drawing. /// [EditorOrder(1)] public MaterialBase Material { get => _material; set { if (_material == value) return; _material = value; Object.Destroy(ref _materialInstance); _materialIsDirty = true; } } /// /// Gets or sets the edge offset. /// [EditorOrder(2), Range(0, 1)] public float EdgeOffset { get => _edgeOffset; set { _edgeOffset = Math.Clamp(value, 0, 1); _materialIsDirty = true; PerformLayout(); } } /// /// Gets or sets the thickness. /// [EditorOrder(3), Range(0, 1), VisibleIf(nameof(ShowMatProp))] public float Thickness { get => _thickness; set { _thickness = Math.Clamp(value, 0, 1); _materialIsDirty = true; PerformLayout(); } } /// /// Gets or sets control background color (transparent color (alpha=0) means no background rendering). /// [VisibleIf(nameof(ShowMatProp))] public new Color BackgroundColor { get => base.BackgroundColor; set { _materialIsDirty = true; base.BackgroundColor = value; } } /// /// Gets or sets the color of the highlight. /// [VisibleIf(nameof(ShowMatProp))] public Color HighlightColor { get => _highlightColor; set { _materialIsDirty = true; _highlightColor = value; } } /// /// Gets or sets the color of the foreground. /// [VisibleIf(nameof(ShowMatProp))] public Color ForegroundColor { get => _foregroundColor; set { _materialIsDirty = true; _foregroundColor = value; } } /// /// Gets or sets the color of the selection. /// [VisibleIf(nameof(ShowMatProp))] public Color SelectionColor { get => _selectionColor; set { _materialIsDirty = true; _selectionColor = value; } } /// /// The selected callback /// [HideInEditor] public Action Selected; /// /// The allow change selection when inside /// [VisibleIf(nameof(ShowMatProp))] public bool AllowChangeSelectionWhenInside; /// /// The center as button /// [VisibleIf(nameof(ShowMatProp))] public bool CenterAsButton; /// /// Initializes a new instance of the class. /// public RadialMenu() : this(0, 0) { } /// /// Initializes a new instance of the class. /// /// Position X coordinate /// Position Y coordinate /// Width /// Height public RadialMenu(float x, float y, float width = 100, float height = 100) : base(x, y, width, height) { AutoFocus = true; var style = Style.Current; if (style != null) { BackgroundColor = style.BackgroundNormal; HighlightColor = style.BackgroundSelected; ForegroundColor = style.BackgroundHighlighted; SelectionColor = style.BackgroundSelected; } _material = Content.LoadAsyncInternal("Engine/DefaultRadialMenu"); } /// /// Initializes a new instance of the class. /// /// Position /// Size public RadialMenu(Float2 location, Float2 size) : this(location.X, location.Y, size.X, size.Y) { } /// public override void DrawSelf() { if (_materialInstance == null && Material != null) { _materialInstance = Material.CreateVirtualInstance(); _materialIsDirty = true; } if (_materialInstance != null) { if (_materialIsDirty) { _materialInstance.SetParameterValue("RadialMenu_EdgeOffset", Math.Clamp(1 - _edgeOffset, 0, 1)); _materialInstance.SetParameterValue("RadialMenu_Thickness", Math.Clamp(_thickness, 0, 1)); _materialInstance.SetParameterValue("RadialMenu_Angle", (1.0f / _segmentCount) * Mathf.Pi); _materialInstance.SetParameterValue("RadialMenu_SegmentCount", _segmentCount); _materialInstance.SetParameterValue("RadialMenu_HighlightColor", _highlightColor); _materialInstance.SetParameterValue("RadialMenu_ForegroundColor", _foregroundColor); _materialInstance.SetParameterValue("RadialMenu_BackgroundColor", BackgroundColor); _materialInstance.SetParameterValue("RadialMenu_Rotation", -_angle + Mathf.Pi); UpdateSelectionColor(); _materialIsDirty = false; } Render2D.DrawMaterial(_materialInstance, new Rectangle(Float2.Zero, new Float2(Size.X < Size.Y ? Size.X : Size.Y))); } } /// public override void OnMouseMove(Float2 location) { if (_materialInstance != null) { if (_highlightSegment == -1) { var min = ((1 - _edgeOffset) - _thickness) * USize * 0.5f; var max = (1 - _edgeOffset) * USize * 0.5f; var val = ((USize * 0.5f) - location).Length; if (Mathf.IsInRange(val, min, max) || val < min && AllowChangeSelectionWhenInside) { UpdateAngle(ref location); } } } base.OnMouseMove(location); } /// public override bool OnMouseDown(Float2 location, MouseButton button) { if (_materialInstance == null) return base.OnMouseDown(location, button); var min = ((1 - _edgeOffset) - _thickness) * USize * 0.5f; var max = (1 - _edgeOffset) * USize * 0.5f; var val = ((USize * 0.5f) - location).Length; var c = val < min && CenterAsButton; var selected = (int)_selectedSegment; selected++; if (Mathf.IsInRange(val, min, max) || c) { _highlightSegment = c ? 0 : selected; UpdateSelectionColor(); return true; } else { _highlightSegment = -1; UpdateSelectionColor(); } return base.OnMouseDown(location, button); } /// public override bool OnMouseUp(Float2 location, MouseButton button) { if (_materialInstance == null) return base.OnMouseDown(location, button); if (_highlightSegment >= 0) { Selected?.Invoke(_highlightSegment); _highlightSegment = -1; UpdateSelectionColor(); var min = ((1 - _edgeOffset) - _thickness) * USize * 0.5f; var max = (1 - _edgeOffset) * USize * 0.5f; var val = ((USize * 0.5f) - location).Length; if (Mathf.IsInRange(val, min, max) || val < min && AllowChangeSelectionWhenInside) { UpdateAngle(ref location); } } return base.OnMouseUp(location, button); } /// public override void OnMouseLeave() { if (_materialInstance == null) return; _selectedSegment = 0; _highlightSegment = -1; Selected?.Invoke(_highlightSegment); UpdateSelectionColor(); } /// public override void OnChildrenChanged() { _segmentCount = 0; for (int i = 0; i < Children.Count; i++) { if (Children[i] is Image) { _segmentCount++; } } _materialIsDirty = true; base.OnChildrenChanged(); } /// public override void PerformLayout(bool force = false) { var sa = -1.0f / _segmentCount * Mathf.TwoPi; var midp = USize * 0.5f; var mp = ((1 - _edgeOffset) - (_thickness * 0.5f)) * midp; float f = 0; if (_segmentCount % 2 != 0) { f += sa * 0.5f; } if (_materialInstance == null) { for (int i = 0; i < Children.Count; i++) { Children[i].Center = Rotate2D(new Float2(0, mp), f) + midp; f += sa; } } else { for (int i = 0; i < Children.Count; i++) { if (Children[i] is Image) { Children[i].Center = Rotate2D(new Float2(0, mp), f) + midp; f += sa; } } } base.PerformLayout(force); } private void UpdateSelectionColor() { Color color; if (_highlightSegment == -1) { color = _foregroundColor; } else { if (CenterAsButton) { color = _highlightSegment > 0 ? SelectionColor : _foregroundColor; } else { color = SelectionColor; } } _materialInstance.SetParameterValue("RadialMenu_SelectionColor", color); } private void UpdateAngle(ref Float2 location) { var size = new Float2(USize); var p = (size * 0.5f) - location; var sa = (1.0f / _segmentCount) * Mathf.TwoPi; _angle = Mathf.Atan2(p.X, p.Y); _angle = Mathf.Ceil((_angle - (sa * 0.5f)) / sa) * sa; _selectedSegment = _angle; _selectedSegment = Mathf.RoundToInt((_selectedSegment < 0 ? Mathf.TwoPi + _selectedSegment : _selectedSegment) / sa); if (float.IsNaN(_angle) || float.IsInfinity(_angle)) _angle = 0; _materialInstance.SetParameterValue("RadialMenu_Rotation", -_angle + Mathf.Pi); } private static Float2 Rotate2D(Float2 point, float angle) { return new Float2(Mathf.Cos(angle) * point.X + Mathf.Sin(angle) * point.Y, Mathf.Cos(angle) * point.Y - Mathf.Sin(angle) * point.X); } } }