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 ;
2022-04-25 15:25:58 +02:00
using MLEM.Graphics ;
2024-07-19 20:02:28 +02:00
using MLEM.Maths ;
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>
2022-12-22 19:18:33 +01:00
public TokenizedString TokenizedText {
get {
this . CheckTextChange ( ) ;
this . TokenizeIfNecessary ( ) ;
return this . tokenizedText ;
}
}
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>
2022-12-23 13:25:56 +01:00
public StyleProp < float > TextScale {
get = > this . textScale ;
set {
this . textScale = value ;
this . SetTextDirty ( ) ;
}
}
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>
2022-12-23 13:25:56 +01:00
public float TextScaleMultiplier {
get = > this . textScaleMultiplier ;
set {
if ( this . textScaleMultiplier ! = value ) {
this . textScaleMultiplier = value ;
this . SetTextDirty ( ) ;
}
}
}
2021-02-28 14:43:07 +01:00
/// <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 {
2022-12-22 19:18:33 +01:00
this . CheckTextChange ( ) ;
return this . displayedText ;
2019-08-09 19:28:48 +02:00
}
2022-12-23 15:08:40 +01:00
set {
this . explicitlySetText = value ;
this . CheckTextChange ( ) ;
}
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>
2022-12-23 13:25:56 +01:00
public bool AutoAdjustWidth {
get = > this . autoAdjustWidth ;
set {
if ( this . autoAdjustWidth ! = value ) {
this . autoAdjustWidth = value ;
this . SetAreaDirty ( ) ;
}
}
}
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>
2022-12-23 13:25:56 +01:00
public bool TruncateIfLong {
get = > this . truncateIfLong ;
set {
if ( this . truncateIfLong ! = value ) {
this . truncateIfLong = value ;
this . SetAlignSplitDirty ( ) ;
}
}
}
2021-05-18 16:47:38 +02:00
/// <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>
2022-12-23 13:25:56 +01:00
public string Ellipsis {
get = > this . ellipsis ;
set {
if ( this . ellipsis ! = value ) {
this . ellipsis = value ;
this . SetAlignSplitDirty ( ) ;
}
}
}
2021-05-18 16:47:38 +02:00
/// <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
}
}
2023-05-18 21:41:36 +02:00
/// <summary>
/// The inclusive index in this paragraph's <see cref="Text"/> to start drawing at.
2024-04-10 20:45:16 +02:00
/// This value is passed to <see cref="TokenizedString.Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.SpriteBatch,Microsoft.Xna.Framework.Vector2,MLEM.Font.GenericFont,Microsoft.Xna.Framework.Color,Vector2,float,float,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Graphics.SpriteEffects,System.Nullable{int},System.Nullable{int})"/>.
2023-05-18 21:41:36 +02:00
/// </summary>
public int? DrawStartIndex ;
/// <summary>
/// The exclusive index in this paragraph's <see cref="Text"/> to stop drawing at.
2024-04-10 20:45:16 +02:00
/// This value is passed to <see cref="TokenizedString.Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.SpriteBatch,Microsoft.Xna.Framework.Vector2,MLEM.Font.GenericFont,Microsoft.Xna.Framework.Color,Vector2,float,float,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Graphics.SpriteEffects,System.Nullable{int},System.Nullable{int})"/>.
2023-05-18 21:41:36 +02:00
/// </summary>
public int? DrawEndIndex ;
2021-06-25 15:23:30 +02:00
2022-05-04 13:22:24 +02:00
/// <inheritdoc />
2022-12-21 21:47:49 +01:00
public override bool IsHidden = > base . IsHidden | | string . IsNullOrWhiteSpace ( this . Text ) ;
2022-05-04 13:22:24 +02:00
2022-12-22 19:18:33 +01:00
private string displayedText ;
private string explicitlySetText ;
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-12-23 13:25:56 +01:00
private StyleProp < float > textScale ;
2022-12-22 19:18:33 +01:00
private TokenizedString tokenizedText ;
2022-12-22 20:04:38 +01:00
private float? lastAlignSplitWidth ;
private float? lastAlignSplitScale ;
2022-12-23 13:25:56 +01:00
private string ellipsis = "..." ;
private bool truncateIfLong ;
private float textScaleMultiplier = 1 ;
private bool autoAdjustWidth ;
2019-08-16 19:08:36 +02:00
2023-12-28 17:16:31 +01: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>
/// <param name="alignment">The paragraph's text alignment.</param>
/// <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 , TextAlignment alignment , bool autoAdjustWidth = false ) : this ( anchor , width , textCallback , autoAdjustWidth ) {
this . Alignment = alignment ;
}
/// <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="text">The paragraph's text</param>
/// <param name="alignment">The paragraph's text alignment.</param>
/// <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 , string text , TextAlignment alignment , bool autoAdjustWidth = false ) : this ( anchor , width , text , autoAdjustWidth ) {
this . Alignment = alignment ;
}
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
2023-12-28 17:16:31 +01: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="text">The paragraph's text</param>
/// <param name="autoAdjustWidth">Whether the paragraph's width should automatically be calculated based on the text within it.</param>
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
}
2023-04-06 15:54:24 +02:00
/// <inheritdoc />
public override void SetAreaAndUpdateChildren ( RectangleF area ) {
base . SetAreaAndUpdateChildren ( area ) ;
// in case an outside source sets our area, we still want to display our text correctly
this . AlignAndSplitIfNecessary ( area . Size ) ;
}
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 ) ;
2022-12-23 14:36:31 +01:00
this . CheckTextChange ( ) ;
2022-12-22 19:50:50 +01:00
this . TokenizeIfNecessary ( ) ;
this . AlignAndSplitIfNecessary ( size ) ;
2024-04-10 23:52:35 +02:00
var textSize = this . tokenizedText . GetArea ( scale : this . TextScale * this . TextScaleMultiplier * this . Scale ) . Size ;
2023-04-06 15:54:24 +02:00
// if we auto-adjust our width, then we would also split the same way with our adjusted width, so cache that
if ( this . AutoAdjustWidth )
this . lastAlignSplitWidth = textSize . X ;
2022-06-24 14:01:26 +02:00
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 ) {
2022-12-22 19:18:33 +01:00
this . TokenizedText ? . Update ( time ) ;
2022-12-23 14:54:02 +01:00
base . 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 ;
2024-04-10 20:27:00 +02:00
this . TokenizedText . Draw ( time , batch , pos , this . RegularFont , color , sc , 0 , 0 , Vector2 . Zero , SpriteEffects . None , this . DrawStartIndex , this . DrawEndIndex ) ;
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
}
2022-12-22 19:18:33 +01:00
private 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
2023-11-11 12:06:28 +01:00
if ( ! this . AreaDirty & & ( this . System = = null | | ! this . CalcActualSize ( this . ParentArea ) . Equals ( this . DisplayArea . Size , Element . Epsilon ) ) )
2022-01-06 23:26:14 +01:00
this . SetAreaDirty ( ) ;
}
2022-12-22 19:18:33 +01:00
private void CheckTextChange ( ) {
var newText = this . GetTextCallback ? . Invoke ( this ) ? ? this . explicitlySetText ;
if ( this . displayedText = = newText )
2022-12-21 21:47:49 +01:00
return ;
2022-12-22 19:18:33 +01:00
var emptyChanged = string . IsNullOrWhiteSpace ( this . displayedText ) ! = string . IsNullOrWhiteSpace ( newText ) ;
this . displayedText = newText ;
2022-12-21 21:47:49 +01:00
if ( emptyChanged )
this . SetAreaDirty ( ) ;
this . SetTextDirty ( ) ;
2020-05-17 00:59:15 +02:00
}
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 ;
}
2022-12-22 19:18:33 +01:00
private void TokenizeIfNecessary ( ) {
if ( this . tokenizedText ! = null )
return ;
// tokenize the text
this . tokenizedText = this . System . TextFormatter . Tokenize ( this . RegularFont , this . Text , this . Alignment ) ;
2022-12-23 13:25:56 +01:00
this . SetAlignSplitDirty ( ) ;
2022-12-22 19:18:33 +01:00
// add links to the paragraph
this . RemoveChildren ( c = > c is Link ) ;
foreach ( var link in this . tokenizedText . Tokens . Where ( t = > t . AppliedCodes . Any ( c = > c is LinkCode ) ) )
this . AddChild ( new Link ( Anchor . TopLeft , link , this . TextScale * this . TextScaleMultiplier ) ) ;
}
2022-12-22 19:50:50 +01:00
private void AlignAndSplitIfNecessary ( Vector2 size ) {
2022-12-22 19:18:33 +01:00
var width = size . X - this . ScaledPadding . Width ;
var scale = this . TextScale * this . TextScaleMultiplier * this . Scale ;
2022-12-22 20:04:38 +01:00
2023-04-06 15:54:24 +02:00
if ( this . lastAlignSplitWidth ? . Equals ( width , Element . Epsilon ) = = true & & this . lastAlignSplitScale ? . Equals ( scale , Element . Epsilon ) = = true )
2022-12-22 20:04:38 +01:00
return ;
this . lastAlignSplitWidth = width ;
this . lastAlignSplitScale = scale ;
2022-12-22 19:18:33 +01:00
if ( this . TruncateIfLong ) {
this . tokenizedText . Truncate ( this . RegularFont , width , scale , this . Ellipsis , this . Alignment ) ;
} else {
this . tokenizedText . Split ( this . RegularFont , width , scale , this . Alignment ) ;
}
}
2022-12-23 13:25:56 +01:00
private void SetAlignSplitDirty ( ) {
this . lastAlignSplitWidth = null ;
this . lastAlignSplitScale = null ;
}
2021-06-25 15:23:30 +02:00
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
}