2021-05-30 17:57:39 +02:00
using System ;
2019-08-09 19:28:48 +02:00
using System.Linq ;
using Microsoft.Xna.Framework ;
using Microsoft.Xna.Framework.Graphics ;
2022-01-06 23:26:14 +01:00
using MLEM.Extensions ;
2019-08-09 19:28:48 +02:00
using MLEM.Font ;
2019-09-06 12:20:53 +02:00
using MLEM.Formatting ;
2020-05-15 22:15:24 +02:00
using MLEM.Formatting.Codes ;
2022-04-25 15:25:58 +02:00
using MLEM.Graphics ;
2019-11-02 14:53:59 +01:00
using MLEM.Misc ;
2019-08-10 21:37:10 +02:00
using MLEM.Ui.Style ;
2019-08-09 19:28:48 +02:00
namespace MLEM.Ui.Elements {
2020-05-22 17:02:24 +02:00
/// <summary>
/// A paragraph element for use inside of a <see cref="UiSystem"/>.
/// A paragraph is an element that contains text.
/// A paragraph's text can be formatted using the ui system's <see cref="UiSystem.TextFormatter"/>.
/// </summary>
2019-08-09 19:28:48 +02:00
public class Paragraph : Element {
2020-05-22 17:02:24 +02:00
/// <summary>
/// The font that this paragraph draws text with.
/// To set its bold and italic font, use <see cref="GenericFont.Bold"/> and <see cref="GenericFont.Italic"/>.
/// </summary>
2021-12-21 00:01:57 +01:00
public StyleProp < GenericFont > RegularFont {
get = > this . regularFont ;
set {
this . regularFont = value ;
2022-01-06 23:26:14 +01:00
this . SetTextDirty ( ) ;
2021-12-21 00:01:57 +01:00
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// The tokenized version of the <see cref="Text"/>
/// </summary>
2020-05-15 19:55:59 +02:00
public TokenizedString TokenizedText { get ; private set ; }
2020-05-22 17:02:24 +02:00
/// <summary>
/// The color that the text will be rendered with
/// </summary>
2019-10-14 21:28:12 +02:00
public StyleProp < Color > TextColor ;
2020-05-22 17:02:24 +02:00
/// <summary>
2021-02-28 14:43:07 +01:00
/// The scale that the text will be rendered with.
/// To add a multiplier rather than changing the scale directly, use <see cref="TextScaleMultiplier"/>.
2020-05-22 17:02:24 +02:00
/// </summary>
2019-10-14 21:28:12 +02:00
public StyleProp < float > TextScale ;
2020-05-22 17:02:24 +02:00
/// <summary>
2021-02-28 14:43:07 +01:00
/// A multiplier that will be applied to <see cref="TextScale"/>.
/// To change the text scale itself, use <see cref="TextScale"/>.
/// </summary>
public float TextScaleMultiplier = 1 ;
/// <summary>
2020-05-22 17:02:24 +02:00
/// The text to render inside of this paragraph.
/// Use <see cref="GetTextCallback"/> if the text changes frequently.
/// </summary>
2019-08-09 19:28:48 +02:00
public string Text {
2020-05-17 00:59:15 +02:00
get {
this . QueryTextCallback ( ) ;
return this . text ;
}
2019-08-09 19:28:48 +02:00
set {
2019-08-16 19:08:36 +02:00
if ( this . text ! = value ) {
this . text = value ;
2022-01-06 23:26:14 +01:00
this . SetTextDirty ( ) ;
2022-06-15 11:38:11 +02:00
2022-05-04 13:54:15 +02:00
var force = string . IsNullOrWhiteSpace ( this . text ) ;
if ( this . forceHide ! = force ) {
this . forceHide = force ;
this . SetAreaDirty ( ) ;
}
2019-08-16 19:08:36 +02:00
}
2019-08-09 19:28:48 +02:00
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// If this paragraph should automatically adjust its width based on the width of the text within it
/// </summary>
2019-08-15 14:59:15 +02:00
public bool AutoAdjustWidth ;
2020-05-22 17:02:24 +02:00
/// <summary>
2021-05-18 16:47:38 +02:00
/// Whether this paragraph should be truncated instead of split if the displayed <see cref="Text"/>'s width exceeds the provided width.
/// When the string is truncated, the <see cref="Ellipsis"/> is added to its end.
/// </summary>
public bool TruncateIfLong ;
/// <summary>
/// The ellipsis characters to use if <see cref="TruncateIfLong"/> is enabled and the string is truncated.
/// If this is set to an empty string, no ellipsis will be attached to the truncated string.
/// </summary>
public string Ellipsis = "..." ;
/// <summary>
2020-05-22 17:02:24 +02:00
/// An event that gets called when this paragraph's <see cref="Text"/> is queried.
/// Use this event for setting this paragraph's text if it changes frequently.
/// </summary>
2019-08-16 19:08:36 +02:00
public TextCallback GetTextCallback ;
2021-05-30 17:57:39 +02:00
/// <summary>
/// The action that is executed if <see cref="Link"/> objects inside of this paragraph are pressed.
/// By default, <see cref="MlemPlatform.OpenLinkOrFile"/> is executed.
/// </summary>
public Action < Link , LinkCode > LinkAction ;
2021-06-25 15:23:30 +02:00
/// <summary>
/// The <see cref="TextAlignment"/> that this paragraph's text should be rendered with
/// </summary>
2022-01-22 22:54:47 +01:00
public StyleProp < TextAlignment > Alignment {
2021-06-25 15:23:30 +02:00
get = > this . alignment ;
set {
this . alignment = value ;
2022-01-06 23:26:14 +01:00
this . SetTextDirty ( ) ;
2021-06-25 15:23:30 +02:00
}
}
2022-05-04 13:22:24 +02:00
/// <inheritdoc />
public override bool IsHidden = > base . IsHidden | | this . forceHide ;
2021-06-25 15:23:30 +02:00
private string text ;
2022-01-22 22:54:47 +01:00
private StyleProp < TextAlignment > alignment ;
2021-12-21 00:01:57 +01:00
private StyleProp < GenericFont > regularFont ;
2022-05-04 13:22:24 +02:00
private bool forceHide ;
2019-08-16 19:08:36 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// Creates a new paragraph with the given settings.
/// </summary>
/// <param name="anchor">The paragraph's anchor</param>
/// <param name="width">The paragraph's width. Note that its height is automatically calculated.</param>
/// <param name="textCallback">The paragraph's text</param>
2020-09-30 22:49:09 +02:00
/// <param name="autoAdjustWidth">Whether the paragraph's width should automatically be calculated based on the text within it.</param>
2022-05-04 13:22:24 +02:00
public Paragraph ( Anchor anchor , float width , TextCallback textCallback , bool autoAdjustWidth = false ) : this ( anchor , width , string . Empty , autoAdjustWidth ) {
2019-08-16 19:08:36 +02:00
this . GetTextCallback = textCallback ;
}
2019-08-09 19:28:48 +02:00
2020-05-22 17:02:24 +02:00
/// <inheritdoc cref="Paragraph(Anchor,float,TextCallback,bool)"/>
2020-09-30 22:49:09 +02:00
public Paragraph ( Anchor anchor , float width , string text , bool autoAdjustWidth = false ) : base ( anchor , new Vector2 ( width , 0 ) ) {
2019-09-26 19:35:22 +02:00
this . Text = text ;
2020-09-30 22:49:09 +02:00
this . AutoAdjustWidth = autoAdjustWidth ;
2019-08-28 18:27:17 +02:00
this . CanBeSelected = false ;
this . CanBeMoused = false ;
2019-08-09 19:28:48 +02:00
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2019-11-02 14:53:59 +01:00
protected override Vector2 CalcActualSize ( RectangleF parentArea ) {
2019-08-09 19:28:48 +02:00
var size = base . CalcActualSize ( parentArea ) ;
2020-05-17 00:59:15 +02:00
this . ParseText ( size ) ;
2022-06-24 14:01:26 +02:00
var textSize = this . TokenizedText . Measure ( this . RegularFont ) * this . TextScale * this . TextScaleMultiplier * this . Scale ;
return new Vector2 ( this . AutoAdjustWidth ? textSize . X + this . ScaledPadding . Width : size . X , textSize . Y + this . ScaledPadding . Height ) ;
2019-08-09 19:28:48 +02:00
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2019-08-16 19:08:36 +02:00
public override void Update ( GameTime time ) {
2020-05-17 00:59:15 +02:00
this . QueryTextCallback ( ) ;
2019-08-16 19:08:36 +02:00
base . Update ( time ) ;
2020-05-17 00:10:29 +02:00
if ( this . TokenizedText ! = null )
2020-05-15 22:15:24 +02:00
this . TokenizedText . Update ( time ) ;
2019-08-16 19:08:36 +02:00
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2022-04-25 15:25:58 +02:00
public override void Draw ( GameTime time , SpriteBatch batch , float alpha , SpriteBatchContext context ) {
2021-11-22 18:52:52 +01:00
var pos = this . DisplayArea . Location + new Vector2 ( this . GetAlignmentOffset ( ) , 0 ) ;
2021-02-28 14:43:07 +01:00
var sc = this . TextScale * this . TextScaleMultiplier * this . Scale ;
2019-11-05 13:28:41 +01:00
var color = this . TextColor . OrDefault ( Color . White ) * alpha ;
2021-11-22 18:52:52 +01:00
this . TokenizedText . Draw ( time , batch , pos , this . RegularFont , color , sc , 0 ) ;
2022-04-25 15:25:58 +02:00
base . Draw ( time , batch , alpha , context ) ;
2019-08-09 19:28:48 +02:00
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2019-08-10 21:37:10 +02:00
protected override void InitStyle ( UiStyle style ) {
base . InitStyle ( style ) ;
2021-12-21 11:54:32 +01:00
this . RegularFont = this . RegularFont . OrStyle ( style . Font ? ? throw new NotSupportedException ( "Paragraphs cannot use ui styles that don't have a font. Please supply a custom font by setting UiStyle.Font." ) ) ;
this . TextScale = this . TextScale . OrStyle ( style . TextScale ) ;
this . TextColor = this . TextColor . OrStyle ( style . TextColor ) ;
2022-01-22 22:54:47 +01:00
this . Alignment = this . Alignment . OrStyle ( style . TextAlignment ) ;
2019-08-10 21:37:10 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Parses this paragraph's <see cref="Text"/> into <see cref="TokenizedText"/>.
/// Additionally, this method adds any <see cref="Link"/> elements for tokenized links in the text.
/// </summary>
/// <param name="size">The paragraph's default size</param>
2020-05-17 00:59:15 +02:00
protected virtual void ParseText ( Vector2 size ) {
2020-06-30 00:49:42 +02:00
if ( this . TokenizedText = = null ) {
// tokenize the text
2021-06-25 15:23:30 +02:00
this . TokenizedText = this . System . TextFormatter . Tokenize ( this . RegularFont , this . Text , this . Alignment ) ;
2020-05-17 00:59:15 +02:00
2020-06-30 00:49:42 +02:00
// add links to the paragraph
2020-05-17 00:59:15 +02:00
this . RemoveChildren ( c = > c is Link ) ;
2020-06-30 00:49:42 +02:00
foreach ( var link in this . TokenizedText . Tokens . Where ( t = > t . AppliedCodes . Any ( c = > c is LinkCode ) ) )
2021-02-28 14:43:07 +01:00
this . AddChild ( new Link ( Anchor . TopLeft , link , this . TextScale * this . TextScaleMultiplier ) ) ;
2020-05-17 00:59:15 +02:00
}
2021-05-18 16:47:38 +02:00
var width = size . X - this . ScaledPadding . Width ;
var scale = this . TextScale * this . TextScaleMultiplier * this . Scale ;
if ( this . TruncateIfLong ) {
2021-06-25 15:23:30 +02:00
this . TokenizedText . Truncate ( this . RegularFont , width , scale , this . Ellipsis , this . Alignment ) ;
2021-05-18 16:47:38 +02:00
} else {
2021-06-25 15:23:30 +02:00
this . TokenizedText . Split ( this . RegularFont , width , scale , this . Alignment ) ;
2021-05-18 16:47:38 +02:00
}
2020-05-17 00:59:15 +02:00
}
2022-01-06 23:26:14 +01:00
/// <summary>
/// A helper method that causes the <see cref="TokenizedText"/> to be reset.
/// Additionally, <see cref="Element.SetAreaDirty"/> if this paragraph's area has changed enough to warrant it, or if it has any <see cref="Link"/> children.
/// </summary>
protected void SetTextDirty ( ) {
this . TokenizedText = null ;
2022-01-09 01:12:16 +01:00
// only set our area dirty if our size changed as a result of this action
2022-06-15 11:38:11 +02:00
if ( ! this . AreaDirty & & ! this . CalcActualSize ( this . ParentArea ) . Equals ( this . DisplayArea . Size , Element . Epsilon ) )
2022-01-06 23:26:14 +01:00
this . SetAreaDirty ( ) ;
}
2020-05-17 00:59:15 +02:00
private void QueryTextCallback ( ) {
if ( this . GetTextCallback ! = null )
this . Text = this . GetTextCallback ( this ) ;
}
2021-06-25 15:23:30 +02:00
private float GetAlignmentOffset ( ) {
2022-01-22 22:54:47 +01:00
switch ( this . Alignment . Value ) {
2021-06-25 15:23:30 +02:00
case TextAlignment . Center :
return this . DisplayArea . Width / 2 ;
case TextAlignment . Right :
return this . DisplayArea . Width ;
}
return 0 ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// A delegate method used for <see cref="Paragraph.GetTextCallback"/>
/// </summary>
/// <param name="paragraph">The current paragraph</param>
2019-08-16 19:08:36 +02:00
public delegate string TextCallback ( Paragraph paragraph ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// A link is a sub-element of the <see cref="Paragraph"/> that is added onto it as a child for any tokens that contain <see cref="LinkCode"/>, to make them selectable and clickable.
/// </summary>
2020-05-17 00:10:29 +02:00
public class Link : Element {
2020-05-22 17:02:24 +02:00
/// <summary>
/// The token that this link represents
/// </summary>
2020-05-17 00:10:29 +02:00
public readonly Token Token ;
2020-06-30 00:49:42 +02:00
private readonly float textScale ;
2020-05-17 00:10:29 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// Creates a new link element with the given settings
/// </summary>
/// <param name="anchor">The link's anchor</param>
/// <param name="token">The token that this link represents</param>
2020-06-30 00:49:42 +02:00
/// <param name="textScale">The scale that text is rendered with</param>
public Link ( Anchor anchor , Token token , float textScale ) : base ( anchor , Vector2 . Zero ) {
2020-05-17 00:10:29 +02:00
this . Token = token ;
2020-06-30 00:49:42 +02:00
this . textScale = textScale ;
2020-05-17 00:10:29 +02:00
this . OnPressed + = e = > {
2021-05-30 17:57:39 +02:00
foreach ( var code in token . AppliedCodes . OfType < LinkCode > ( ) ) {
if ( this . Parent is Paragraph p & & p . LinkAction ! = null ) {
p . LinkAction . Invoke ( this , code ) ;
} else {
MlemPlatform . Current . OpenLinkOrFile ( code . Match . Groups [ 1 ] . Value ) ;
}
}
2020-05-17 00:10:29 +02:00
} ;
}
2020-06-30 00:49:42 +02:00
/// <inheritdoc />
public override void ForceUpdateArea ( ) {
// set the position offset and size to the token's first area
2021-07-03 01:50:37 +02:00
var area = this . Token . GetArea ( Vector2 . Zero , this . textScale ) . FirstOrDefault ( ) ;
2021-07-24 07:36:42 +02:00
if ( this . Parent is Paragraph p )
area . Location + = new Vector2 ( p . GetAlignmentOffset ( ) / p . Scale , 0 ) ;
this . PositionOffset = area . Location ;
2021-07-03 01:50:37 +02:00
this . IsHidden = area . IsEmpty ;
2020-06-30 00:49:42 +02:00
this . Size = area . Size ;
base . ForceUpdateArea ( ) ;
}
/// <inheritdoc />
public override Element GetElementUnderPos ( Vector2 position ) {
var ret = base . GetElementUnderPos ( position ) ;
if ( ret ! = null )
return ret ;
// check if any of our token's parts are hovered
2022-01-07 20:50:32 +01:00
var location = this . Parent . DisplayArea . Location ;
if ( this . Parent is Paragraph p )
location . X + = p . GetAlignmentOffset ( ) ;
foreach ( var rect in this . Token . GetArea ( location , this . Scale * this . textScale ) ) {
2021-11-22 15:13:08 +01:00
if ( rect . Contains ( this . TransformInverse ( position ) ) )
2020-06-30 00:49:42 +02:00
return this ;
}
return null ;
}
2020-05-17 00:10:29 +02:00
}
2019-08-09 19:28:48 +02:00
}
2022-06-17 18:23:47 +02:00
}