2019-08-13 23:54:29 +02:00
using System ;
2022-05-03 18:58:18 +02:00
using System.Collections.Generic ;
2019-08-13 23:54:29 +02:00
using Microsoft.Xna.Framework ;
2022-03-14 14:20:12 +01:00
using MLEM.Input ;
2019-08-13 23:54:29 +02:00
using MLEM.Ui.Style ;
2024-04-10 17:58:01 +02:00
using Color = Microsoft . Xna . Framework . Color ;
2024-07-19 20:02:28 +02:00
using RectangleF = MLEM . Maths . RectangleF ;
2022-08-20 11:39:28 +02:00
#if FNA
2024-07-19 20:02:28 +02:00
using MLEM.Maths ;
2022-08-20 11:39:28 +02:00
#endif
2019-08-13 23:54:29 +02:00
namespace MLEM.Ui.Elements {
2020-05-22 17:02:24 +02:00
/// <summary>
/// A tooltip element for use inside of a <see cref="UiSystem"/>.
/// A tooltip is a <see cref="Panel"/> 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.
/// </summary>
2019-12-31 14:08:13 +01:00
public class Tooltip : Panel {
2019-08-13 23:54:29 +02:00
2022-05-03 18:58:18 +02:00
/// <summary>
/// A list of <see cref="Elements.Paragraph"/> objects that this tooltip automatically manages.
/// A paragraph that is contained in this list will automatically have the <see cref="UiStyle.TooltipTextWidth"/> and <see cref="UiStyle.TooltipTextColor"/> applied.
2022-05-03 19:07:53 +02:00
/// To add a paragraph to both this list and to <see cref="Element.Children"/>, use <see cref="AddParagraph(Elements.Paragraph,int)"/>.
2022-05-03 18:58:18 +02:00
/// </summary>
public readonly List < Paragraph > Paragraphs = new List < Paragraph > ( ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
2024-04-10 17:58:01 +02:00
/// The offset that this tooltip should have from the mouse position
2020-05-22 17:02:24 +02:00
/// </summary>
2019-12-31 14:08:13 +01:00
public StyleProp < Vector2 > MouseOffset ;
2020-05-22 17:02:24 +02:00
/// <summary>
2024-04-10 17:58:01 +02:00
/// The offset that this tooltip should have from the element snapped to when <see cref="DisplayInAutoNavMode"/> is true.
2022-04-05 14:42:30 +02:00
/// </summary>
public StyleProp < Vector2 > AutoNavOffset ;
/// <summary>
2024-04-10 17:58:01 +02:00
/// The anchor that should be used when this tooltip is displayed using the mouse. The <see cref="MouseOffset"/> will be applied.
/// </summary>
public StyleProp < Anchor > MouseAnchor ;
/// <summary>
/// The anchor that should be used when this tooltip is displayed using auto-nav mode. The <see cref="AutoNavOffset"/> will be applied.
/// </summary>
public StyleProp < Anchor > AutoNavAnchor ;
/// <summary>
/// If this is <see langword="true"/>, and the mouse is used, the tooltip will attach to the hovered element in a static position using the <see cref="AutoNavOffset"/> and <see cref="AutoNavAnchor"/> properties, rather than following the mouse cursor exactly.
/// </summary>
public StyleProp < bool > UseAutoNavBehaviorForMouse ;
/// <summary>
2020-12-05 16:42:21 +01:00
/// The amount of time that the mouse has to be over an element before it appears
/// </summary>
public StyleProp < TimeSpan > Delay ;
2022-05-03 19:35:44 +02:00
/// <summary>
/// The <see cref="Elements.Paragraph.TextColor"/> that this tooltip's <see cref="Paragraphs"/> should have
/// </summary>
public StyleProp < Color > ParagraphTextColor {
get = > this . paragraphTextColor ;
set {
this . paragraphTextColor = value ;
this . UpdateParagraphsStyles ( ) ;
}
}
/// <summary>
/// The <see cref="Elements.Paragraph.TextScale"/> that this tooltip's <see cref="Paragraphs"/> should have
/// </summary>
public StyleProp < float > ParagraphTextScale {
get = > this . paragraphTextScale ;
set {
this . paragraphTextScale = value ;
this . UpdateParagraphsStyles ( ) ;
}
}
/// <summary>
/// The width that this tooltip's <see cref="Paragraphs"/> should have
/// </summary>
public StyleProp < float > ParagraphWidth {
get = > this . paragraphWidth ;
set {
this . paragraphWidth = value ;
this . UpdateParagraphsStyles ( ) ;
}
}
2022-03-14 14:20:12 +01:00
/// <summary>
2022-04-05 14:42:30 +02:00
/// Determines whether this tooltip should display when <see cref="UiControls.IsAutoNavMode"/> 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 <see cref="AutoNavOffset"/> applied.
/// </summary>
public bool DisplayInAutoNavMode ;
/// <summary>
2022-03-14 14:20:12 +01:00
/// The position that this tooltip should be following (or snapped to) instead of the <see cref="InputHandler.ViewportMousePosition"/>.
/// If this value is unset, <see cref="InputHandler.ViewportMousePosition"/> will be used as the snap position.
/// Note that <see cref="MouseOffset"/> is still applied with this value set.
2024-04-10 17:58:01 +02:00
/// Note that, if <see cref="UseAutoNavBehaviorForMouse"/> is <see langword="true"/>, this value is ignored.
2022-03-14 14:20:12 +01:00
/// </summary>
public virtual Vector2 ? SnapPosition { get ; set ; }
2024-10-08 18:07:38 +02:00
/// <summary>
/// Determines whether this tooltip should ignore its viewport, which is either this tooltip's <see cref="Viewport"/> or the underlying <see cref="Element.System"/>'s <see cref="UiSystem.Viewport"/>. If this is <see langword="true"/>, the tooltip is allowed to display outside of the viewport, without being bounded in <see cref="SnapPositionToMouse"/>.
/// </summary>
public virtual bool IgnoreViewport { get ; set ; }
/// <summary>
/// The viewport that this tooltip should be bound to. If this value is unset, the underlying <see cref="Element.System"/>'s <see cref="UiSystem.Viewport"/> will be used. Note that, if <see cref="IgnoreViewport"/> is <see langword="true"/>, this value is ignored.
/// </summary>
public virtual Rectangle ? Viewport { get ; set ; }
2022-04-05 14:42:30 +02:00
2022-04-05 14:17:12 +02:00
/// <inheritdoc />
public override bool IsHidden = > this . autoHidden | | base . IsHidden ;
2019-08-13 23:54:29 +02:00
2020-12-05 16:42:21 +01:00
private TimeSpan delayCountdown ;
2021-09-09 16:53:12 +02:00
private bool autoHidden ;
2022-04-05 14:42:30 +02:00
private Element snapElement ;
2022-05-03 19:35:44 +02:00
private StyleProp < float > paragraphWidth ;
private StyleProp < float > paragraphTextScale ;
private StyleProp < Color > paragraphTextColor ;
2020-12-05 16:42:21 +01:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// Creates a new tooltip with the given settings
/// </summary>
/// <param name="text">The text to display on the tooltip</param>
/// <param name="elementToHover">The element that should automatically cause the tooltip to appear and disappear when hovered and not hovered, respectively</param>
2021-10-29 23:33:15 +02:00
public Tooltip ( string text = null , Element elementToHover = null ) :
2019-12-31 14:08:13 +01:00
base ( Anchor . TopLeft , Vector2 . One , Vector2 . Zero ) {
2024-07-17 18:21:13 +02:00
if ( text ! = null )
this . AddParagraph ( text ) ;
2021-01-11 00:09:29 +01:00
this . Init ( elementToHover ) ;
}
2019-08-24 20:45:40 +02:00
2021-01-11 00:09:29 +01:00
/// <summary>
/// Creates a new tooltip with the given settings
/// </summary>
/// <param name="textCallback">The text to display on the tooltip</param>
/// <param name="elementToHover">The element that should automatically cause the tooltip to appear and disappear when hovered and not hovered, respectively</param>
2021-10-29 23:33:15 +02:00
public Tooltip ( Paragraph . TextCallback textCallback , Element elementToHover = null ) :
2021-01-11 00:09:29 +01:00
base ( Anchor . TopLeft , Vector2 . One , Vector2 . Zero ) {
2024-07-17 18:21:13 +02:00
this . AddParagraph ( textCallback ) ;
2021-01-11 00:09:29 +01:00
this . Init ( elementToHover ) ;
2019-08-13 23:54:29 +02:00
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2019-08-13 23:54:29 +02:00
public override void Update ( GameTime time ) {
base . Update ( time ) ;
2019-12-28 14:23:40 +01:00
this . SnapPositionToMouse ( ) ;
2020-12-05 16:42:21 +01:00
2021-09-09 16:53:12 +02:00
if ( this . delayCountdown > TimeSpan . Zero ) {
2020-12-05 16:42:21 +01:00
this . delayCountdown - = time . ElapsedGameTime ;
2021-09-09 16:53:12 +02:00
if ( this . delayCountdown < = TimeSpan . Zero ) {
2020-12-05 16:42:21 +01:00
this . IsHidden = false ;
2021-09-09 16:53:12 +02:00
this . UpdateAutoHidden ( ) ;
2022-04-05 14:54:20 +02:00
this . SnapPositionToMouse ( ) ;
2021-09-09 16:53:12 +02:00
}
} else {
this . UpdateAutoHidden ( ) ;
2020-12-05 16:42:21 +01:00
}
2019-12-28 14:23:40 +01:00
}
2019-08-15 14:59:15 +02:00
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2019-12-28 14:23:40 +01:00
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 ( ) ;
2021-07-22 04:27:57 +02:00
this . SnapPositionToMouse ( ) ;
2019-12-28 14:23:40 +01:00
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2019-12-28 14:23:40 +01:00
protected override void InitStyle ( UiStyle style ) {
base . InitStyle ( style ) ;
2021-12-21 11:54:32 +01:00
this . Texture = this . Texture . OrStyle ( style . TooltipBackground ) ;
this . MouseOffset = this . MouseOffset . OrStyle ( style . TooltipOffset ) ;
2022-04-05 14:42:30 +02:00
this . AutoNavOffset = this . AutoNavOffset . OrStyle ( style . TooltipAutoNavOffset ) ;
2024-04-10 17:58:01 +02:00
this . MouseAnchor = this . MouseAnchor . OrStyle ( style . TooltipMouseAnchor ) ;
this . AutoNavAnchor = this . AutoNavAnchor . OrStyle ( style . TooltipAutoNavAnchor ) ;
this . UseAutoNavBehaviorForMouse = this . UseAutoNavBehaviorForMouse . OrStyle ( style . TooltipUseAutoNavBehaviorForMouse ) ;
2021-12-21 11:54:32 +01:00
this . Delay = this . Delay . OrStyle ( style . TooltipDelay ) ;
2022-05-03 19:35:44 +02:00
this . ParagraphTextColor = this . ParagraphTextColor . OrStyle ( style . TooltipTextColor ) ;
this . ParagraphTextScale = this . ParagraphTextScale . OrStyle ( style . TextScale ) ;
this . ParagraphWidth = this . ParagraphWidth . OrStyle ( style . TooltipTextWidth ) ;
2021-12-21 11:54:32 +01:00
this . ChildPadding = this . ChildPadding . OrStyle ( style . TooltipChildPadding ) ;
2022-05-03 19:35:44 +02:00
this . UpdateParagraphsStyles ( ) ;
2022-05-03 18:58:18 +02:00
}
/// <summary>
/// Adds the given paragraph to this tooltip's managed <see cref="Paragraphs"/> list, as well as to its children using <see cref="Element.AddChild{T}"/>.
/// A paragraph that is contained in the <see cref="Paragraphs"/> list will automatically have the <see cref="UiStyle.TooltipTextWidth"/> and <see cref="UiStyle.TooltipTextColor"/> applied.
/// </summary>
/// <param name="paragraph">The paragraph to add</param>
/// <returns>The added paragraph, for chaining</returns>
2022-05-03 19:07:53 +02:00
/// <param name="index">The index to add the child at, or -1 to add it to the end of the <see cref="Element.Children"/> list</param>
public Paragraph AddParagraph ( Paragraph paragraph , int index = - 1 ) {
2022-05-03 18:58:18 +02:00
this . Paragraphs . Add ( paragraph ) ;
2022-05-03 19:07:53 +02:00
this . AddChild ( paragraph , index ) ;
2022-05-03 19:35:44 +02:00
this . UpdateParagraphStyle ( paragraph ) ;
2022-05-03 18:58:18 +02:00
return paragraph ;
}
/// <summary>
/// Adds a new paragraph with the given text callback to this tooltip's managed <see cref="Paragraphs"/> list, as well as to its children using <see cref="Element.AddChild{T}"/>.
/// A paragraph that is contained in the <see cref="Paragraphs"/> list will automatically have the <see cref="UiStyle.TooltipTextWidth"/> and <see cref="UiStyle.TooltipTextColor"/> applied.
/// </summary>
/// <param name="text">The text that the paragraph should display</param>
/// <returns>The created paragraph, for chaining</returns>
2022-05-03 19:07:53 +02:00
/// <param name="index">The index to add the child at, or -1 to add it to the end of the <see cref="Element.Children"/> list</param>
public Paragraph AddParagraph ( Paragraph . TextCallback text , int index = - 1 ) {
return this . AddParagraph ( new Paragraph ( Anchor . AutoLeft , 0 , text ) , index ) ;
2022-05-03 18:58:18 +02:00
}
/// <summary>
/// Adds a new paragraph with the given text to this tooltip's managed <see cref="Paragraphs"/> list, as well as to its children using <see cref="Element.AddChild{T}"/>.
/// A paragraph that is contained in the <see cref="Paragraphs"/> list will automatically have the <see cref="UiStyle.TooltipTextWidth"/> and <see cref="UiStyle.TooltipTextColor"/> applied.
/// </summary>
/// <param name="text">The text that the paragraph should display</param>
/// <returns>The created paragraph, for chaining</returns>
2022-05-03 19:07:53 +02:00
/// <param name="index">The index to add the child at, or -1 to add it to the end of the <see cref="Element.Children"/> list</param>
public Paragraph AddParagraph ( string text , int index = - 1 ) {
return this . AddParagraph ( p = > text , index ) ;
2019-12-28 14:23:40 +01:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
2023-04-15 15:11:50 +02:00
/// Causes this tooltip's position to be snapped to the mouse position, or the element to snap to if <see cref="DisplayInAutoNavMode"/> is true, or the <see cref="SnapPosition"/> if set.
2020-05-22 17:02:24 +02:00
/// </summary>
2019-12-28 14:23:40 +01:00
public void SnapPositionToMouse ( ) {
2024-10-08 18:07:38 +02:00
Vector2 snap ;
2022-04-05 14:42:30 +02:00
if ( this . snapElement ! = null ) {
2024-10-08 18:07:38 +02:00
snap = this . GetSnapOffset ( this . AutoNavAnchor , this . snapElement . DisplayArea , this . AutoNavOffset ) / this . Scale ;
2022-04-05 14:42:30 +02:00
} else {
2024-04-10 17:58:01 +02:00
var mouseBounds = new RectangleF ( this . SnapPosition ? ? this . Input . ViewportMousePosition . ToVector2 ( ) , Vector2 . Zero ) ;
2024-10-08 18:07:38 +02:00
snap = this . GetSnapOffset ( this . MouseAnchor , mouseBounds , this . MouseOffset ) / this . Scale ;
}
if ( ! this . IgnoreViewport ) {
var view = this . Viewport ? ? this . System . Viewport ;
2024-10-08 18:12:22 +02:00
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 ;
2024-10-08 18:07:38 +02:00
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 ;
2022-04-05 14:42:30 +02:00
}
2024-10-08 18:07:38 +02:00
this . PositionOffset = snap ;
2019-08-13 23:54:29 +02:00
}
2019-12-29 15:18:49 +01:00
2020-12-05 22:42:10 +01:00
/// <summary>
/// Adds this tooltip to the given <see cref="UiSystem"/> and either displays it directly or starts the <see cref="Delay"/> timer.
/// </summary>
/// <param name="system">The system to add this tooltip to</param>
/// <param name="name">The name that this tooltip should use</param>
2022-04-05 14:42:30 +02:00
/// <returns>Whether this tooltip was successfully added, which is not the case if it is already being displayed currently.</returns>
public bool Display ( UiSystem system , string name ) {
if ( system . Add ( name , this ) = = null )
return false ;
2020-12-05 22:42:10 +01:00
if ( this . Delay < = TimeSpan . Zero ) {
this . IsHidden = false ;
this . SnapPositionToMouse ( ) ;
} else {
this . IsHidden = true ;
this . delayCountdown = this . Delay ;
}
2021-09-09 16:53:12 +02:00
this . autoHidden = false ;
2022-04-05 14:42:30 +02:00
return true ;
2020-12-05 22:42:10 +01:00
}
/// <summary>
/// Removes this tooltip from its <see cref="UiSystem"/> and resets the <see cref="Delay"/> timer, if there is one.
/// </summary>
public void Remove ( ) {
this . delayCountdown = TimeSpan . Zero ;
if ( this . System ! = null )
this . System . Remove ( this . Root . Name ) ;
}
2021-02-18 04:16:17 +01:00
/// <summary>
/// Adds this tooltip instance to the given <see cref="Element"/>, making it display when it is moused over
/// </summary>
/// <param name="elementToHover">The element that should automatically cause the tooltip to appear and disappear when hovered and not hovered, respectively</param>
public void AddToElement ( Element elementToHover ) {
2024-04-10 17:58:01 +02:00
// 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
2022-04-05 14:42:30 +02:00
elementToHover . OnSelected + = e = > {
2022-04-09 22:00:21 +02:00
if ( this . DisplayInAutoNavMode & & e . Controls . IsAutoNavMode ) {
2022-04-05 14:42:30 +02:00
this . snapElement = e ;
this . Display ( e . System , $"{e.GetType().Name}Tooltip" ) ;
}
} ;
elementToHover . OnDeselected + = e = > {
if ( this . DisplayInAutoNavMode ) {
this . Remove ( ) ;
this . snapElement = null ;
}
} ;
2021-02-18 04:16:17 +01:00
}
2021-01-11 00:09:29 +01:00
private void Init ( Element elementToHover ) {
this . SetWidthBasedOnChildren = true ;
this . SetHeightBasedOnChildren = true ;
this . CanBeMoused = false ;
2021-02-18 04:16:17 +01:00
if ( elementToHover ! = null )
this . AddToElement ( elementToHover ) ;
2021-01-11 00:09:29 +01:00
}
2021-09-09 16:53:12 +02:00
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 ;
2022-04-05 14:17:12 +02:00
this . SetAreaDirty ( ) ;
2021-09-09 16:53:12 +02:00
}
}
2022-05-03 19:35:44 +02:00
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 ) ;
2022-05-03 18:58:18 +02:00
paragraph . AutoAdjustWidth = true ;
}
2024-04-10 17:58:01 +02:00
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" ) ;
}
}
2019-08-13 23:54:29 +02:00
}
2022-06-17 18:23:47 +02:00
}