using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using MLEM.Input; using MLEM.Ui.Style; using Color = Microsoft.Xna.Framework.Color; using RectangleF = MLEM.Maths.RectangleF; #if FNA using MLEM.Maths; #endif namespace MLEM.Ui.Elements { /// /// A tooltip element for use inside of a . /// A tooltip is a with a custom cursor that always follows the position of the mouse. /// Tooltips can easily be configured to be hooked onto an element, causing them to appear when it is moused, and disappear when it is not moused anymore. /// public class Tooltip : Panel { /// /// A list of objects that this tooltip automatically manages. /// A paragraph that is contained in this list will automatically have the and applied. /// To add a paragraph to both this list and to , use . /// public readonly List Paragraphs = new List(); /// /// The offset that this tooltip should have from the mouse position /// public StyleProp MouseOffset; /// /// The offset that this tooltip should have from the element snapped to when is true. /// public StyleProp AutoNavOffset; /// /// The anchor that should be used when this tooltip is displayed using the mouse. The will be applied. /// public StyleProp MouseAnchor; /// /// The anchor that should be used when this tooltip is displayed using auto-nav mode. The will be applied. /// public StyleProp AutoNavAnchor; /// /// If this is , and the mouse is used, the tooltip will attach to the hovered element in a static position using the and properties, rather than following the mouse cursor exactly. /// public StyleProp UseAutoNavBehaviorForMouse; /// /// The amount of time that the mouse has to be over an element before it appears /// public StyleProp Delay; /// /// The that this tooltip's should have /// public StyleProp ParagraphTextColor { get => this.paragraphTextColor; set { this.paragraphTextColor = value; this.UpdateParagraphsStyles(); } } /// /// The that this tooltip's should have /// public StyleProp ParagraphTextScale { get => this.paragraphTextScale; set { this.paragraphTextScale = value; this.UpdateParagraphsStyles(); } } /// /// The width that this tooltip's should have /// public StyleProp ParagraphWidth { get => this.paragraphWidth; set { this.paragraphWidth = value; this.UpdateParagraphsStyles(); } } /// /// Determines whether this tooltip should display when is true, which is when the UI is being controlled using a keyboard or gamepad. /// If this tooltip is displayed in auto-nav mode, it will display below the selected element with the applied. /// public bool DisplayInAutoNavMode; /// /// The position that this tooltip should be following (or snapped to) instead of the . /// If this value is unset, will be used as the snap position. /// Note that is still applied with this value set. /// Note that, if is , this value is ignored. /// public virtual Vector2? SnapPosition { get; set; } /// /// Determines whether this tooltip should ignore its viewport, which is either this tooltip's or the underlying 's . If this is , the tooltip is allowed to display outside of the viewport, without being bounded in . /// public virtual bool IgnoreViewport { get; set; } /// /// The viewport that this tooltip should be bound to. If this value is unset, the underlying 's will be used. Note that, if is , this value is ignored. /// public virtual Rectangle? Viewport { get; set; } /// public override bool IsHidden => this.autoHidden || base.IsHidden; private TimeSpan delayCountdown; private bool autoHidden; private Element snapElement; private StyleProp paragraphWidth; private StyleProp paragraphTextScale; private StyleProp paragraphTextColor; /// /// Creates a new tooltip with the given settings /// /// The text to display on the tooltip /// The element that should automatically cause the tooltip to appear and disappear when hovered and not hovered, respectively public Tooltip(string text = null, Element elementToHover = null) : base(Anchor.TopLeft, Vector2.One, Vector2.Zero) { if (text != null) this.AddParagraph(text); this.Init(elementToHover); } /// /// Creates a new tooltip with the given settings /// /// The text to display on the tooltip /// The element that should automatically cause the tooltip to appear and disappear when hovered and not hovered, respectively public Tooltip(Paragraph.TextCallback textCallback, Element elementToHover = null) : base(Anchor.TopLeft, Vector2.One, Vector2.Zero) { this.AddParagraph(textCallback); this.Init(elementToHover); } /// public override void Update(GameTime time) { base.Update(time); this.SnapPositionToMouse(); if (this.delayCountdown > TimeSpan.Zero) { this.delayCountdown -= time.ElapsedGameTime; if (this.delayCountdown <= TimeSpan.Zero) { this.IsHidden = false; this.UpdateAutoHidden(); this.SnapPositionToMouse(); } } else { this.UpdateAutoHidden(); } } /// public override void ForceUpdateArea() { if (this.Parent != null) throw new NotSupportedException($"A tooltip shouldn't be the child of another element ({this.Parent})"); base.ForceUpdateArea(); this.SnapPositionToMouse(); } /// protected override void InitStyle(UiStyle style) { base.InitStyle(style); this.Texture = this.Texture.OrStyle(style.TooltipBackground); this.MouseOffset = this.MouseOffset.OrStyle(style.TooltipOffset); this.AutoNavOffset = this.AutoNavOffset.OrStyle(style.TooltipAutoNavOffset); this.MouseAnchor = this.MouseAnchor.OrStyle(style.TooltipMouseAnchor); this.AutoNavAnchor = this.AutoNavAnchor.OrStyle(style.TooltipAutoNavAnchor); this.UseAutoNavBehaviorForMouse = this.UseAutoNavBehaviorForMouse.OrStyle(style.TooltipUseAutoNavBehaviorForMouse); this.Delay = this.Delay.OrStyle(style.TooltipDelay); this.ParagraphTextColor = this.ParagraphTextColor.OrStyle(style.TooltipTextColor); this.ParagraphTextScale = this.ParagraphTextScale.OrStyle(style.TextScale); this.ParagraphWidth = this.ParagraphWidth.OrStyle(style.TooltipTextWidth); this.ChildPadding = this.ChildPadding.OrStyle(style.TooltipChildPadding); this.UpdateParagraphsStyles(); } /// /// Adds the given paragraph to this tooltip's managed list, as well as to its children using . /// A paragraph that is contained in the list will automatically have the and applied. /// /// The paragraph to add /// The added paragraph, for chaining /// The index to add the child at, or -1 to add it to the end of the list public Paragraph AddParagraph(Paragraph paragraph, int index = -1) { this.Paragraphs.Add(paragraph); this.AddChild(paragraph, index); this.UpdateParagraphStyle(paragraph); return paragraph; } /// /// Adds a new paragraph with the given text callback to this tooltip's managed list, as well as to its children using . /// A paragraph that is contained in the list will automatically have the and applied. /// /// The text that the paragraph should display /// The created paragraph, for chaining /// The index to add the child at, or -1 to add it to the end of the list public Paragraph AddParagraph(Paragraph.TextCallback text, int index = -1) { return this.AddParagraph(new Paragraph(Anchor.AutoLeft, 0, text), index); } /// /// Adds a new paragraph with the given text to this tooltip's managed list, as well as to its children using . /// A paragraph that is contained in the list will automatically have the and applied. /// /// The text that the paragraph should display /// The created paragraph, for chaining /// The index to add the child at, or -1 to add it to the end of the list public Paragraph AddParagraph(string text, int index = -1) { return this.AddParagraph(p => text, index); } /// /// Causes this tooltip's position to be snapped to the mouse position, or the element to snap to if is true, or the if set. /// public void SnapPositionToMouse() { Vector2 snap; if (this.snapElement != null) { snap = this.GetSnapOffset(this.AutoNavAnchor, this.snapElement.DisplayArea, this.AutoNavOffset) / this.Scale; } else { var mouseBounds = new RectangleF(this.SnapPosition ?? this.Input.ViewportMousePosition.ToVector2(), Vector2.Zero); snap = this.GetSnapOffset(this.MouseAnchor, mouseBounds, this.MouseOffset) / this.Scale; } if (!this.IgnoreViewport) { var view = this.Viewport ?? this.System.Viewport; if (snap.X * this.Scale < view.X) snap.X = view.X / this.Scale; if (snap.Y * this.Scale < view.Y) snap.Y = view.Y / this.Scale; if (snap.X * this.Scale + this.Area.Width >= view.Right) snap.X = (view.Right - this.Area.Width) / this.Scale; if (snap.Y * this.Scale + this.Area.Height >= view.Bottom) snap.Y = (view.Bottom - this.Area.Height) / this.Scale; } this.PositionOffset = snap; } /// /// Adds this tooltip to the given and either displays it directly or starts the timer. /// /// The system to add this tooltip to /// The name that this tooltip should use /// Whether this tooltip was successfully added, which is not the case if it is already being displayed currently. public bool Display(UiSystem system, string name) { if (system.Add(name, this) == null) return false; if (this.Delay <= TimeSpan.Zero) { this.IsHidden = false; this.SnapPositionToMouse(); } else { this.IsHidden = true; this.delayCountdown = this.Delay; } this.autoHidden = false; return true; } /// /// Removes this tooltip from its and resets the timer, if there is one. /// public void Remove() { this.delayCountdown = TimeSpan.Zero; if (this.System != null) this.System.Remove(this.Root.Name); } /// /// Adds this tooltip instance to the given , making it display when it is moused over /// /// The element that should automatically cause the tooltip to appear and disappear when hovered and not hovered, respectively public void AddToElement(Element elementToHover) { // mouse controls elementToHover.OnMouseEnter += e => { if (this.UseAutoNavBehaviorForMouse) this.snapElement = e; this.Display(e.System, $"{e.GetType().Name}Tooltip"); }; elementToHover.OnMouseExit += e => { this.Remove(); if (this.UseAutoNavBehaviorForMouse) this.snapElement = null; }; // auto-nav controls elementToHover.OnSelected += e => { if (this.DisplayInAutoNavMode && e.Controls.IsAutoNavMode) { this.snapElement = e; this.Display(e.System, $"{e.GetType().Name}Tooltip"); } }; elementToHover.OnDeselected += e => { if (this.DisplayInAutoNavMode) { this.Remove(); this.snapElement = null; } }; } private void Init(Element elementToHover) { this.SetWidthBasedOnChildren = true; this.SetHeightBasedOnChildren = true; this.CanBeMoused = false; if (elementToHover != null) this.AddToElement(elementToHover); } private void UpdateAutoHidden() { var shouldBeHidden = true; foreach (var child in this.Children) { if (!child.IsHidden) { shouldBeHidden = false; break; } } if (this.autoHidden != shouldBeHidden) { this.autoHidden = shouldBeHidden; this.SetAreaDirty(); } } private void UpdateParagraphsStyles() { foreach (var paragraph in this.Paragraphs) this.UpdateParagraphStyle(paragraph); } private void UpdateParagraphStyle(Paragraph paragraph) { paragraph.TextColor = paragraph.TextColor.OrStyle(this.ParagraphTextColor, 1); paragraph.TextScale = paragraph.TextScale.OrStyle(this.ParagraphTextScale, 1); paragraph.Size = new Vector2(this.ParagraphWidth, 0); paragraph.AutoAdjustWidth = true; } private Vector2 GetSnapOffset(Anchor anchor, RectangleF snapBounds, Vector2 offset) { switch (anchor) { case Anchor.TopLeft: return snapBounds.Location - this.DisplayArea.Size - offset; case Anchor.TopCenter: return new Vector2(snapBounds.Center.X - this.DisplayArea.Width / 2F, snapBounds.Top - this.DisplayArea.Height) - offset; case Anchor.TopRight: return new Vector2(snapBounds.Right + offset.X, snapBounds.Top - this.DisplayArea.Height - offset.Y); case Anchor.CenterLeft: return new Vector2(snapBounds.X - this.DisplayArea.Width - offset.X, snapBounds.Center.Y - this.DisplayArea.Height / 2 + offset.Y); case Anchor.Center: return snapBounds.Center - this.DisplayArea.Size / 2 + offset; case Anchor.CenterRight: return new Vector2(snapBounds.Right, snapBounds.Center.Y - this.DisplayArea.Height / 2) + offset; case Anchor.BottomLeft: return new Vector2(snapBounds.X - this.DisplayArea.Width - offset.X, snapBounds.Bottom + offset.Y); case Anchor.BottomCenter: return new Vector2(snapBounds.Center.X - this.DisplayArea.Width / 2F, snapBounds.Bottom) + offset; case Anchor.BottomRight: return snapBounds.Location + snapBounds.Size + offset; default: throw new NotSupportedException($"Tooltip anchors don't support the {anchor} value"); } } } }