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; }
///
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 snapPosition;
if (this.snapElement != null) {
snapPosition = this.GetSnapOffset(this.AutoNavAnchor, this.snapElement.DisplayArea, this.AutoNavOffset);
} else {
var mouseBounds = new RectangleF(this.SnapPosition ?? this.Input.ViewportMousePosition.ToVector2(), Vector2.Zero);
snapPosition = this.GetSnapOffset(this.MouseAnchor, mouseBounds, this.MouseOffset);
}
var viewport = this.System.Viewport;
var offset = snapPosition / this.Scale;
if (offset.X < viewport.X)
offset.X = viewport.X;
if (offset.Y < viewport.Y)
offset.Y = viewport.Y;
if (offset.X * this.Scale + this.Area.Width >= viewport.Right)
offset.X = (viewport.Right - this.Area.Width) / this.Scale;
if (offset.Y * this.Scale + this.Area.Height >= viewport.Bottom)
offset.Y = (viewport.Bottom - this.Area.Height) / this.Scale;
this.PositionOffset = offset;
}
///
/// 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");
}
}
}
}