A game framework written with osu! in mind.
at master 197 lines 12 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.Graphics.Sprites; 7 8namespace osu.Framework.Graphics.Containers 9{ 10 /// <inheritdoc /> 11 public class CustomizableTextContainer : CustomizableTextContainer<SpriteText> 12 { 13 protected override SpriteText CreateSpriteText() => new SpriteText(); 14 } 15 16 /// <summary> 17 /// A <see cref="TextFlowContainer"/> that supports adding icons into its text. Inherit from this class to define reusable custom placeholders for icons. 18 /// </summary> 19 public abstract class CustomizableTextContainer<T> : TextFlowContainer<T> 20 where T : SpriteText 21 { 22 private const string unescaped_left = "["; 23 private const string escaped_left = "[["; 24 25 private const string unescaped_right = "]"; 26 private const string escaped_right = "]]"; 27 28 public static string Escape(string text) => text.Replace(unescaped_left, escaped_left).Replace(unescaped_right, escaped_right); 29 30 public static string Unescape(string text) => text.Replace(escaped_left, unescaped_left).Replace(escaped_right, unescaped_right); 31 32 /// <summary> 33 /// Sets the placeholders that should be used to replace the numeric placeholders, in the order given. 34 /// </summary> 35 public IEnumerable<Drawable> Placeholders 36 { 37 set 38 { 39 if (value == null) 40 throw new ArgumentNullException(nameof(value)); 41 42 placeholders.Clear(); 43 placeholders.AddRange(value); 44 } 45 } 46 47 private readonly List<Drawable> placeholders = new List<Drawable>(); 48 private readonly Dictionary<string, Delegate> iconFactories = new Dictionary<string, Delegate>(); 49 50 /// <summary> 51 /// Adds the given drawable as a placeholder that can be used when adding text. The drawable must not have a parent. Returns the index that can be used to reference the added placeholder. 52 /// </summary> 53 /// <param name="drawable">The drawable to use as a placeholder. This drawable must not have a parent.</param> 54 /// <returns>The index that can be used to reference the added placeholder.</returns> 55 public int AddPlaceholder(Drawable drawable) 56 { 57 placeholders.Add(drawable); 58 return placeholders.Count - 1; 59 } 60 61 /// <summary> 62 /// Adds the given factory method as a placeholder. It will be used to create a drawable each time [<paramref name="name"/>] is encountered in the text. The <paramref name="factory"/> method must return a <see cref="Drawable"/> and may contain an arbitrary number of integer parameters. If there are, fe, 2 integer parameters on the factory method, the placeholder in the text would need to look like [<paramref name="name"/>(42, 1337)] supplying the values 42 and 1337 to the method as arguments. 63 /// </summary> 64 /// <param name="name">The name of the placeholder that the factory should create drawables for.</param> 65 /// <param name="factory">The factory method creating drawables.</param> 66 protected void AddIconFactory(string name, Delegate factory) => iconFactories.Add(name, factory); 67 68 // I dislike the following overloads as much as you, but if we only had the general overload taking a Delegate, AddIconFactory("test", someInstanceMethod) would not compile (because we would need to cast someInstanceMethod to a delegate type first). 69 /// <summary> 70 /// Adds the given factory method as a placeholder. It will be used to create a drawable each time [<paramref name="name"/>] is encountered in the text. The <paramref name="factory"/> method must return a <see cref="Drawable"/> and may contain an arbitrary number of integer parameters. If there are, fe, 2 integer parameters on the factory method, the placeholder in the text would need to look like [<paramref name="name"/>(42, 1337)] supplying the values 42 and 1337 to the method as arguments. 71 /// </summary> 72 /// <param name="name">The name of the placeholder that the factory should create drawables for.</param> 73 /// <param name="factory">The factory method creating drawables.</param> 74 protected void AddIconFactory(string name, Func<Drawable> factory) => iconFactories.Add(name, factory); 75 76 /// <summary> 77 /// Adds the given factory method as a placeholder. It will be used to create a drawable each time [<paramref name="name"/>] is encountered in the text. The <paramref name="factory"/> method must return a <see cref="Drawable"/> and may contain an arbitrary number of integer parameters. If there are, fe, 2 integer parameters on the factory method, the placeholder in the text would need to look like [<paramref name="name"/>(42, 1337)] supplying the values 42 and 1337 to the method as arguments. 78 /// </summary> 79 /// <param name="name">The name of the placeholder that the factory should create drawables for.</param> 80 /// <param name="factory">The factory method creating drawables.</param> 81 protected void AddIconFactory(string name, Func<int, Drawable> factory) => iconFactories.Add(name, factory); 82 83 /// <summary> 84 /// Adds the given factory method as a placeholder. It will be used to create a drawable each time [<paramref name="name"/>] is encountered in the text. The <paramref name="factory"/> method must return a <see cref="Drawable"/> and may contain an arbitrary number of integer parameters. If there are, fe, 2 integer parameters on the factory method, the placeholder in the text would need to look like [<paramref name="name"/>(42, 1337)] supplying the values 42 and 1337 to the method as arguments. 85 /// </summary> 86 /// <param name="name">The name of the placeholder that the factory should create drawables for.</param> 87 /// <param name="factory">The factory method creating drawables.</param> 88 protected void AddIconFactory(string name, Func<int, int, Drawable> factory) => iconFactories.Add(name, factory); 89 90 internal override IEnumerable<Drawable> AddLine(TextChunk<T> chunk) 91 { 92 if (!chunk.NewLineIsParagraph) 93 AddInternal(new NewLineContainer(true)); 94 95 var sprites = new List<Drawable>(); 96 int index = 0; 97 string str = chunk.Text; 98 99 while (index < str.Length) 100 { 101 Drawable placeholderDrawable = null; 102 int nextPlaceholderIndex = str.IndexOf(unescaped_left, index, StringComparison.Ordinal); 103 // make sure we skip ahead to the next [ as long as the current [ is escaped 104 while (nextPlaceholderIndex != -1 && str.IndexOf(escaped_left, nextPlaceholderIndex, StringComparison.Ordinal) == nextPlaceholderIndex) 105 nextPlaceholderIndex = str.IndexOf(unescaped_left, nextPlaceholderIndex + 2, StringComparison.Ordinal); 106 107 string strPiece = null; 108 109 if (nextPlaceholderIndex != -1) 110 { 111 int placeholderEnd = str.IndexOf(unescaped_right, nextPlaceholderIndex, StringComparison.Ordinal); 112 // make sure we skip ahead to the next ] as long as the current ] is escaped 113 while (placeholderEnd != -1 && str.IndexOf(escaped_right, placeholderEnd, StringComparison.InvariantCulture) == placeholderEnd) 114 placeholderEnd = str.IndexOf(unescaped_right, placeholderEnd + 2, StringComparison.Ordinal); 115 116 if (placeholderEnd != -1) 117 { 118 strPiece = str[index..nextPlaceholderIndex]; 119 string placeholderStr = str.AsSpan(nextPlaceholderIndex + 1, placeholderEnd - nextPlaceholderIndex - 1).Trim().ToString(); 120 string placeholderName = placeholderStr; 121 string paramStr = ""; 122 int parensOpen = placeholderStr.IndexOf('('); 123 124 if (parensOpen != -1) 125 { 126 placeholderName = placeholderStr.AsSpan(0, parensOpen).Trim().ToString(); 127 int parensClose = placeholderStr.IndexOf(')', parensOpen); 128 if (parensClose != -1) 129 paramStr = placeholderStr.AsSpan(parensOpen + 1, parensClose - parensOpen - 1).Trim().ToString(); 130 else 131 throw new ArgumentException($"Missing ) in placeholder {placeholderStr}."); 132 } 133 134 if (int.TryParse(placeholderStr, out int placeholderIndex)) 135 { 136 if (placeholderIndex >= placeholders.Count) 137 throw new ArgumentException($"This text has {placeholders.Count} placeholders. But placeholder with index {placeholderIndex} was used."); 138 if (placeholderIndex < 0) 139 throw new ArgumentException($"Negative placeholder indices are invalid. Index {placeholderIndex} was used."); 140 141 placeholderDrawable = placeholders[placeholderIndex]; 142 } 143 else 144 { 145 object[] args; 146 147 if (string.IsNullOrWhiteSpace(paramStr)) 148 { 149 args = Array.Empty<object>(); 150 } 151 else 152 { 153 string[] argStrs = paramStr.Split(','); 154 args = new object[argStrs.Length]; 155 156 for (int i = 0; i < argStrs.Length; ++i) 157 { 158 if (!int.TryParse(argStrs[i], out int argVal)) 159 throw new ArgumentException($"The argument \"{argStrs[i]}\" in placeholder {placeholderStr} is not an integer."); 160 161 args[i] = argVal; 162 } 163 } 164 165 if (!iconFactories.TryGetValue(placeholderName, out Delegate cb)) 166 throw new ArgumentException($"There is no placeholder named {placeholderName}."); 167 168 placeholderDrawable = (Drawable)cb.DynamicInvoke(args); 169 } 170 171 index = placeholderEnd + 1; 172 } 173 } 174 175 if (strPiece == null) 176 { 177 strPiece = str.Substring(index); 178 index = str.Length; 179 } 180 181 // unescape stuff 182 strPiece = Unescape(strPiece); 183 sprites.AddRange(AddString(new TextChunk<T>(strPiece, chunk.NewLineIsParagraph, chunk.CreationParameters))); 184 185 if (placeholderDrawable != null) 186 { 187 if (placeholderDrawable.Parent != null) 188 throw new ArgumentException("All icons used by a customizable text container must not have a parent. If you get this error message it means one of your icon factories created a drawable that was already added to another parent, or you used a drawable as a placeholder that already has another parent or you used an index-based placeholder (like [2]) more than once."); 189 190 AddInternal(placeholderDrawable); 191 } 192 } 193 194 return sprites; 195 } 196 } 197}