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 ;
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 ;
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>
2020-03-28 22:25:06 +01:00
public StyleProp < GenericFont > RegularFont ;
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 ; }
2019-10-14 21:28:12 +02:00
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 ;
2019-09-13 11:53:28 +02:00
this . IsHidden = string . IsNullOrWhiteSpace ( this . text ) ;
2019-08-16 19:08:36 +02:00
this . SetAreaDirty ( ) ;
2020-05-17 00:59:15 +02:00
// force text to be re-tokenized
2020-05-17 00:10:29 +02:00
this . TokenizedText = null ;
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>
public TextAlignment Alignment {
get = > this . alignment ;
set {
this . alignment = value ;
this . SetAreaDirty ( ) ;
this . TokenizedText = null ;
}
}
private string text ;
private TextAlignment alignment ;
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>
public Paragraph ( Anchor anchor , float width , TextCallback textCallback , bool autoAdjustWidth = false )
: this ( anchor , width , "" , autoAdjustWidth ) {
2019-08-16 19:08:36 +02:00
this . GetTextCallback = textCallback ;
this . Text = textCallback ( this ) ;
2019-09-26 19:35:22 +02:00
if ( this . Text = = null )
this . IsHidden = true ;
2019-08-16 19:08:36 +02:00
}
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 ;
if ( this . Text = = null )
this . IsHidden = true ;
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 ) ;
2021-03-24 22:44:39 +01:00
var ( w , h ) = this . TokenizedText . Measure ( this . RegularFont ) * this . TextScale * this . TextScaleMultiplier * this . Scale ;
return new Vector2 ( this . AutoAdjustWidth ? w + this . ScaledPadding . Width : size . X , h + 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 />
2019-09-20 13:22:05 +02:00
public override void Draw ( GameTime time , SpriteBatch batch , float alpha , BlendState blendState , SamplerState samplerState , Matrix matrix ) {
2021-06-25 15:23:30 +02:00
var pos = this . DisplayArea . Location + new Vector2 ( 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-06-25 15:23:30 +02:00
this . TokenizedText . Draw ( time , batch , pos , this . RegularFont , color , sc , 0 , this . Alignment ) ;
2019-09-20 13:22:05 +02:00
base . Draw ( time , batch , alpha , blendState , samplerState , matrix ) ;
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 ) ;
2019-10-14 21:28:12 +02:00
this . TextScale . SetFromStyle ( style . TextScale ) ;
2020-06-30 00:49:42 +02:00
this . RegularFont . SetFromStyle ( style . Font ) ;
2020-09-13 18:00:19 +02:00
this . TextColor . SetFromStyle ( style . TextColor ) ;
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
}
private void QueryTextCallback ( ) {
if ( this . GetTextCallback ! = null )
this . Text = this . GetTextCallback ( this ) ;
}
2021-06-25 15:23:30 +02:00
private float GetAlignmentOffset ( ) {
switch ( this . Alignment ) {
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-06-25 15:23:30 +02:00
this . PositionOffset = area . Location + new Vector2 ( ( ( Paragraph ) this . Parent ) . GetAlignmentOffset ( ) / this . Parent . Scale , 0 ) ;
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
foreach ( var rect in this . Token . GetArea ( this . Parent . DisplayArea . Location , this . Scale * this . textScale ) ) {
if ( rect . Contains ( position ) )
return this ;
}
return null ;
}
2020-05-17 00:10:29 +02:00
}
2019-08-09 19:28:48 +02:00
}
}