A game framework written with osu! in mind.
at master 650 lines 22 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using System; 5using System.Collections.Generic; 6using osu.Framework.Allocation; 7using osu.Framework.Bindables; 8using osu.Framework.Development; 9using osu.Framework.Extensions.IEnumerableExtensions; 10using osu.Framework.Graphics.Containers; 11using osu.Framework.Graphics.Shaders; 12using osu.Framework.Graphics.UserInterface; 13using osu.Framework.IO.Stores; 14using osu.Framework.Layout; 15using osu.Framework.Localisation; 16using osu.Framework.Utils; 17using osu.Framework.Text; 18using osuTK; 19using osuTK.Graphics; 20 21namespace osu.Framework.Graphics.Sprites 22{ 23 /// <summary> 24 /// A container for simple text rendering purposes. If more complex text rendering is required, use <see cref="TextFlowContainer"/> instead. 25 /// </summary> 26 public partial class SpriteText : Drawable, IHasLineBaseHeight, ITexturedShaderDrawable, IHasText, IHasFilterTerms, IFillFlowContainer, IHasCurrentValue<string> 27 { 28 private const float default_text_size = 20; 29 30 /// <remarks> 31 /// <c>U+00A0</c> is the Unicode NON-BREAKING SPACE character (distinct from the standard ASCII space). 32 /// </remarks> 33 private static readonly char[] default_never_fixed_width_characters = { '.', ',', ':', ' ', '\u00A0' }; 34 35 [Resolved] 36 private FontStore store { get; set; } 37 38 [Resolved] 39 private LocalisationManager localisation { get; set; } 40 41 private ILocalisedBindableString localisedText; 42 43 public IShader TextureShader { get; private set; } 44 public IShader RoundedTextureShader { get; private set; } 45 46 public SpriteText() 47 { 48 current.BindValueChanged(text => 49 { 50 // importantly, to avoid a feedback loop which will overwrite a localised text object, check equality of the resulting text before propagating a basic string to Text. 51 // in the case localisedText is not yet setup, special consideration does not need to be given as it can be assumed the change to current was a user invoked change. 52 if (localisedText == null || text.NewValue != localisedText.Value) 53 Text = text.NewValue; 54 }); 55 56 AddLayout(charactersCache); 57 AddLayout(parentScreenSpaceCache); 58 AddLayout(localScreenSpaceCache); 59 AddLayout(shadowOffsetCache); 60 AddLayout(textBuilderCache); 61 } 62 63 [BackgroundDependencyLoader] 64 private void load(ShaderManager shaders) 65 { 66 localisedText = localisation.GetLocalisedString(text); 67 68 TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); 69 RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); 70 71 // Pre-cache the characters in the texture store 72 foreach (var character in localisedText.Value) 73 { 74 var unused = store.Get(font.FontName, character) ?? store.Get(null, character); 75 } 76 } 77 78 protected override void LoadComplete() 79 { 80 base.LoadComplete(); 81 82 localisedText.BindValueChanged(str => 83 { 84 current.Value = localisedText.Value; 85 86 if (string.IsNullOrEmpty(str.NewValue)) 87 { 88 // We'll become not present and won't update the characters to set the size to 0, so do it manually 89 if (requiresAutoSizedWidth) 90 base.Width = Padding.TotalHorizontal; 91 if (requiresAutoSizedHeight) 92 base.Height = Padding.TotalVertical; 93 } 94 95 invalidate(true); 96 }, true); 97 } 98 99 private LocalisableString text = string.Empty; 100 101 /// <summary> 102 /// Gets or sets the text to be displayed. 103 /// </summary> 104 public LocalisableString Text 105 { 106 get => text; 107 set 108 { 109 if (text.Equals(value)) 110 return; 111 112 text = value; 113 114 if (localisedText != null) 115 { 116 localisedText.Text = value; 117 } 118 } 119 } 120 121 private readonly BindableWithCurrent<string> current = new BindableWithCurrent<string>(); 122 123 public Bindable<string> Current 124 { 125 get => current.Current; 126 set => current.Current = value; 127 } 128 129 private string displayedText => localisedText?.Value ?? text.ToString(); 130 131 private FontUsage font = FontUsage.Default; 132 133 /// <summary> 134 /// Contains information on the font used to display the text. 135 /// </summary> 136 public FontUsage Font 137 { 138 get => font; 139 set 140 { 141 font = value; 142 143 invalidate(true, true); 144 shadowOffsetCache.Invalidate(); 145 } 146 } 147 148 private bool allowMultiline = true; 149 150 /// <summary> 151 /// True if the text should be wrapped if it gets too wide. Note that \n does NOT cause a line break. If you need explicit line breaks, use <see cref="TextFlowContainer"/> instead. 152 /// </summary> 153 /// <remarks> 154 /// If enabled, <see cref="Truncate"/> will be disabled. 155 /// </remarks> 156 public bool AllowMultiline 157 { 158 get => allowMultiline; 159 set 160 { 161 if (allowMultiline == value) 162 return; 163 164 if (value) 165 Truncate = false; 166 167 allowMultiline = value; 168 invalidate(true, true); 169 } 170 } 171 172 private bool shadow; 173 174 /// <summary> 175 /// True if a shadow should be displayed around the text. 176 /// </summary> 177 public bool Shadow 178 { 179 get => shadow; 180 set 181 { 182 if (shadow == value) 183 return; 184 185 shadow = value; 186 187 Invalidate(Invalidation.DrawNode); 188 } 189 } 190 191 private Color4 shadowColour = new Color4(0, 0, 0, 0.2f); 192 193 /// <summary> 194 /// The colour of the shadow displayed around the text. A shadow will only be displayed if the <see cref="Shadow"/> property is set to true. 195 /// </summary> 196 public Color4 ShadowColour 197 { 198 get => shadowColour; 199 set 200 { 201 if (shadowColour == value) 202 return; 203 204 shadowColour = value; 205 206 Invalidate(Invalidation.DrawNode); 207 } 208 } 209 210 private Vector2 shadowOffset = new Vector2(0, 0.06f); 211 212 /// <summary> 213 /// The offset of the shadow displayed around the text. A shadow will only be displayed if the <see cref="Shadow"/> property is set to true. 214 /// </summary> 215 public Vector2 ShadowOffset 216 { 217 get => shadowOffset; 218 set 219 { 220 if (shadowOffset == value) 221 return; 222 223 shadowOffset = value; 224 225 invalidate(true); 226 shadowOffsetCache.Invalidate(); 227 } 228 } 229 230 private bool useFullGlyphHeight = true; 231 232 /// <summary> 233 /// True if the <see cref="SpriteText"/>'s vertical size should be equal to <see cref="FontUsage.Size"/> (the full height) or precisely the size of used characters. 234 /// Set to false to allow better centering of individual characters/numerals/etc. 235 /// </summary> 236 public bool UseFullGlyphHeight 237 { 238 get => useFullGlyphHeight; 239 set 240 { 241 if (useFullGlyphHeight == value) 242 return; 243 244 useFullGlyphHeight = value; 245 246 invalidate(true, true); 247 } 248 } 249 250 private bool truncate; 251 252 /// <summary> 253 /// If true, text should be truncated when it exceeds the <see cref="Drawable.DrawWidth"/> of this <see cref="SpriteText"/>. 254 /// </summary> 255 /// <remarks> 256 /// Has no effect if no <see cref="Width"/> or custom sizing is set. 257 /// If enabled, <see cref="AllowMultiline"/> will be disabled. 258 /// </remarks> 259 public bool Truncate 260 { 261 get => truncate; 262 set 263 { 264 if (truncate == value) return; 265 266 if (value) 267 AllowMultiline = false; 268 269 truncate = value; 270 invalidate(true, true); 271 } 272 } 273 274 private string ellipsisString = "…"; 275 276 /// <summary> 277 /// When <see cref="Truncate"/> is enabled, this decides what string is used to signify that truncation has occured. 278 /// Defaults to "…". 279 /// </summary> 280 public string EllipsisString 281 { 282 get => ellipsisString; 283 set 284 { 285 if (ellipsisString == value) return; 286 287 ellipsisString = value; 288 invalidate(true, true); 289 } 290 } 291 292 private bool requiresAutoSizedWidth => explicitWidth == null && (RelativeSizeAxes & Axes.X) == 0; 293 294 private bool requiresAutoSizedHeight => explicitHeight == null && (RelativeSizeAxes & Axes.Y) == 0; 295 296 private float? explicitWidth; 297 298 /// <summary> 299 /// Gets or sets the width of this <see cref="SpriteText"/>. The <see cref="SpriteText"/> will maintain this width when set. 300 /// </summary> 301 public override float Width 302 { 303 get 304 { 305 if (requiresAutoSizedWidth) 306 computeCharacters(); 307 return base.Width; 308 } 309 set 310 { 311 if (explicitWidth == value) 312 return; 313 314 base.Width = value; 315 explicitWidth = value; 316 317 invalidate(true, true); 318 } 319 } 320 321 private float maxWidth = float.PositiveInfinity; 322 323 /// <summary> 324 /// The maximum width of this <see cref="SpriteText"/>. Affects both auto and fixed sizing modes. 325 /// </summary> 326 /// <remarks> 327 /// This becomes a relative value if this <see cref="SpriteText"/> is relatively-sized on the X-axis. 328 /// </remarks> 329 public float MaxWidth 330 { 331 get => maxWidth; 332 set 333 { 334 if (maxWidth == value) 335 return; 336 337 maxWidth = value; 338 invalidate(true, true); 339 } 340 } 341 342 private float? explicitHeight; 343 344 /// <summary> 345 /// Gets or sets the height of this <see cref="SpriteText"/>. The <see cref="SpriteText"/> will maintain this height when set. 346 /// </summary> 347 public override float Height 348 { 349 get 350 { 351 if (requiresAutoSizedHeight) 352 computeCharacters(); 353 return base.Height; 354 } 355 set 356 { 357 if (explicitHeight == value) 358 return; 359 360 base.Height = value; 361 explicitHeight = value; 362 363 invalidate(true, true); 364 } 365 } 366 367 /// <summary> 368 /// Gets or sets the size of this <see cref="SpriteText"/>. The <see cref="SpriteText"/> will maintain this size when set. 369 /// </summary> 370 public override Vector2 Size 371 { 372 get 373 { 374 if (requiresAutoSizedWidth || requiresAutoSizedHeight) 375 computeCharacters(); 376 return base.Size; 377 } 378 set 379 { 380 Width = value.X; 381 Height = value.Y; 382 } 383 } 384 385 private Vector2 spacing; 386 387 /// <summary> 388 /// Gets or sets the spacing between characters of this <see cref="SpriteText"/>. 389 /// </summary> 390 public Vector2 Spacing 391 { 392 get => spacing; 393 set 394 { 395 if (spacing == value) 396 return; 397 398 spacing = value; 399 400 invalidate(true, true); 401 } 402 } 403 404 private MarginPadding padding; 405 406 /// <summary> 407 /// Shrinks the space which may be occupied by characters of this <see cref="SpriteText"/> by the specified amount on each side. 408 /// </summary> 409 public MarginPadding Padding 410 { 411 get => padding; 412 set 413 { 414 if (padding.Equals(value)) 415 return; 416 417 if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Padding)} must be finite, but is {value}."); 418 419 padding = value; 420 421 invalidate(true, true); 422 } 423 } 424 425 public override bool IsPresent => base.IsPresent && (AlwaysPresent || !string.IsNullOrEmpty(displayedText)); 426 427 #region Characters 428 429 private readonly LayoutValue charactersCache = new LayoutValue(Invalidation.DrawSize | Invalidation.Presence, InvalidationSource.Parent); 430 431 /// <summary> 432 /// Glyph list to be passed to <see cref="TextBuilder"/>. 433 /// </summary> 434 private readonly List<TextBuilderGlyph> charactersBacking = new List<TextBuilderGlyph>(); 435 436 /// <summary> 437 /// The characters in local space. 438 /// </summary> 439 private List<TextBuilderGlyph> characters 440 { 441 get 442 { 443 computeCharacters(); 444 return charactersBacking; 445 } 446 } 447 448 /// <summary> 449 /// Compute character textures and positions. 450 /// </summary> 451 private void computeCharacters() 452 { 453 if (LoadState >= LoadState.Loaded) 454 ThreadSafety.EnsureUpdateThread(); 455 456 if (store == null) 457 return; 458 459 if (charactersCache.IsValid) 460 return; 461 462 charactersBacking.Clear(); 463 464 // Todo: Re-enable this assert after autosize is split into two passes. 465 // Debug.Assert(!isComputingCharacters, "Cyclic invocation of computeCharacters()!"); 466 467 Vector2 textBounds = Vector2.Zero; 468 469 try 470 { 471 if (string.IsNullOrEmpty(displayedText)) 472 return; 473 474 TextBuilder textBuilder = getTextBuilder(); 475 476 textBuilder.Reset(); 477 textBuilder.AddText(displayedText); 478 textBounds = textBuilder.Bounds; 479 } 480 finally 481 { 482 if (requiresAutoSizedWidth) 483 base.Width = textBounds.X + Padding.Right; 484 if (requiresAutoSizedHeight) 485 base.Height = textBounds.Y + Padding.Bottom; 486 487 base.Width = Math.Min(base.Width, MaxWidth); 488 489 charactersCache.Validate(); 490 } 491 } 492 493 private readonly LayoutValue parentScreenSpaceCache = new LayoutValue(Invalidation.DrawSize | Invalidation.Presence | Invalidation.DrawInfo, InvalidationSource.Parent); 494 private readonly LayoutValue localScreenSpaceCache = new LayoutValue(Invalidation.MiscGeometry, InvalidationSource.Self); 495 496 private readonly List<ScreenSpaceCharacterPart> screenSpaceCharactersBacking = new List<ScreenSpaceCharacterPart>(); 497 498 /// <summary> 499 /// The characters in screen space. These are ready to be drawn. 500 /// </summary> 501 private List<ScreenSpaceCharacterPart> screenSpaceCharacters 502 { 503 get 504 { 505 computeScreenSpaceCharacters(); 506 return screenSpaceCharactersBacking; 507 } 508 } 509 510 private void computeScreenSpaceCharacters() 511 { 512 if (!parentScreenSpaceCache.IsValid) 513 { 514 localScreenSpaceCache.Invalidate(); 515 parentScreenSpaceCache.Validate(); 516 } 517 518 if (localScreenSpaceCache.IsValid) 519 return; 520 521 screenSpaceCharactersBacking.Clear(); 522 523 Vector2 inflationAmount = DrawInfo.MatrixInverse.ExtractScale().Xy; 524 525 foreach (var character in characters) 526 { 527 screenSpaceCharactersBacking.Add(new ScreenSpaceCharacterPart 528 { 529 DrawQuad = ToScreenSpace(character.DrawRectangle.Inflate(inflationAmount)), 530 InflationPercentage = Vector2.Divide(inflationAmount, character.DrawRectangle.Size), 531 Texture = character.Texture 532 }); 533 } 534 535 localScreenSpaceCache.Validate(); 536 } 537 538 private readonly LayoutValue<Vector2> shadowOffsetCache = new LayoutValue<Vector2>(Invalidation.DrawInfo, InvalidationSource.Parent); 539 540 private Vector2 premultipliedShadowOffset => 541 shadowOffsetCache.IsValid ? shadowOffsetCache.Value : shadowOffsetCache.Value = ToScreenSpace(shadowOffset * Font.Size) - ToScreenSpace(Vector2.Zero); 542 543 #endregion 544 545 #region Invalidation 546 547 private void invalidate(bool characters = false, bool textBuilder = false) 548 { 549 if (characters) 550 charactersCache.Invalidate(); 551 552 if (textBuilder) 553 InvalidateTextBuilder(); 554 555 parentScreenSpaceCache.Invalidate(); 556 localScreenSpaceCache.Invalidate(); 557 558 Invalidate(Invalidation.DrawNode); 559 } 560 561 #endregion 562 563 #region DrawNode 564 565 protected override DrawNode CreateDrawNode() => new SpriteTextDrawNode(this); 566 567 #endregion 568 569 /// <summary> 570 /// The characters that should be excluded from fixed-width application. Defaults to (".", ",", ":", " ") if null. 571 /// </summary> 572 protected virtual char[] FixedWidthExcludeCharacters => null; 573 574 /// <summary> 575 /// The character to use to calculate the fixed width width. Defaults to 'm'. 576 /// </summary> 577 protected virtual char FixedWidthReferenceCharacter => 'm'; 578 579 /// <summary> 580 /// The character to fallback to use if a character glyph lookup failed. 581 /// </summary> 582 protected virtual char FallbackCharacter => '?'; 583 584 private readonly LayoutValue<TextBuilder> textBuilderCache = new LayoutValue<TextBuilder>(Invalidation.DrawSize, InvalidationSource.Parent); 585 586 /// <summary> 587 /// Invalidates the current <see cref="TextBuilder"/>, causing a new one to be created next time it's required via <see cref="CreateTextBuilder"/>. 588 /// </summary> 589 protected void InvalidateTextBuilder() => textBuilderCache.Invalidate(); 590 591 /// <summary> 592 /// Creates a <see cref="TextBuilder"/> to generate the character layout for this <see cref="SpriteText"/>. 593 /// </summary> 594 /// <param name="store">The <see cref="ITexturedGlyphLookupStore"/> where characters should be retrieved from.</param> 595 /// <returns>The <see cref="TextBuilder"/>.</returns> 596 protected virtual TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) 597 { 598 var excludeCharacters = FixedWidthExcludeCharacters ?? default_never_fixed_width_characters; 599 600 float builderMaxWidth = requiresAutoSizedWidth 601 ? MaxWidth 602 : ApplyRelativeAxes(RelativeSizeAxes, new Vector2(Math.Min(MaxWidth, base.Width), base.Height), FillMode).X - Padding.Right; 603 604 if (AllowMultiline) 605 { 606 return new MultilineTextBuilder(store, Font, builderMaxWidth, UseFullGlyphHeight, new Vector2(Padding.Left, Padding.Top), Spacing, charactersBacking, 607 excludeCharacters, FallbackCharacter, FixedWidthReferenceCharacter); 608 } 609 610 if (Truncate) 611 { 612 return new TruncatingTextBuilder(store, Font, builderMaxWidth, ellipsisString, UseFullGlyphHeight, new Vector2(Padding.Left, Padding.Top), Spacing, charactersBacking, 613 excludeCharacters, FallbackCharacter, FixedWidthReferenceCharacter); 614 } 615 616 return new TextBuilder(store, Font, builderMaxWidth, UseFullGlyphHeight, new Vector2(Padding.Left, Padding.Top), Spacing, charactersBacking, 617 excludeCharacters, FallbackCharacter, FixedWidthReferenceCharacter); 618 } 619 620 private TextBuilder getTextBuilder() 621 { 622 if (!textBuilderCache.IsValid) 623 textBuilderCache.Value = CreateTextBuilder(store); 624 625 return textBuilderCache.Value; 626 } 627 628 public override string ToString() => $@"""{displayedText}"" " + base.ToString(); 629 630 /// <summary> 631 /// Gets the base height of the font used by this text. If the font of this text is invalid, 0 is returned. 632 /// </summary> 633 public float LineBaseHeight 634 { 635 get 636 { 637 var baseHeight = store.GetBaseHeight(Font.FontName); 638 if (baseHeight.HasValue) 639 return baseHeight.Value * Font.Size; 640 641 if (string.IsNullOrEmpty(displayedText)) 642 return 0; 643 644 return store.GetBaseHeight(displayedText[0]).GetValueOrDefault() * Font.Size; 645 } 646 } 647 648 public IEnumerable<string> FilterTerms => displayedText.Yield(); 649 } 650}