A game framework written with osu! in mind.
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}