1 /// 2 module glui.rich_label; 3 4 import raylib; 5 6 import std.conv; 7 import std.meta; 8 import std.array; 9 import std.typecons; 10 import std.algorithm; 11 12 import glui.node; 13 import glui.utils; 14 import glui.style; 15 16 alias richLabel = simpleConstructor!GluiRichLabel; 17 18 @safe: 19 20 /// Defines a part of the label text. 21 struct Part { 22 23 /// Style to apply for this part. If null, uses default style instead. 24 Rebindable!(const Style) style; 25 26 /// Text for this part. 27 string text; 28 29 } 30 31 enum isPart(T) = is(T : Style) || is(T == string); 32 33 /// A rich label can display text on the screen and apply custom styling to parts of the text. 34 /// 35 /// Warning: This component is currently difficult to use and easy to break. It is also lacking in features such as 36 /// wrapping. To obtain styles in order to pass to it, it's recommended to create a custom node, define them with 37 /// `DefineStyles` and then use the resulting styles. A way to get achieve this is to subclass this node. 38 /// 39 /// Styles: $(UL 40 /// $(LI `style` = Default style for this node.) 41 /// ) 42 class GluiRichLabel : GluiNode { 43 44 mixin DefineStyles; 45 mixin ImplHoveredRect; 46 47 /// Parts defining label text. 48 Part[] textParts; 49 50 static foreach (index; 0 .. BasicNodeParamLength) { 51 52 /// Initialize the label with given text. 53 /// Params: 54 /// content = Style objects and strings to define parts of the text. 55 this(T...)(BasicNodeParam!index sup, T content) 56 if (allSatisfy!(isPart, T)) { 57 58 super(sup); 59 60 foreach (elem; content) { 61 62 this ~= elem; 63 64 } 65 66 } 67 68 } 69 70 /// Change the style for next part of the text. 71 void opOpAssign(string op : "~")(const Style style) { 72 73 textParts ~= Part(style.rebindable, ""); 74 75 } 76 77 /// Append new text. 78 void opOpAssign(string op : "~", T : string)(T text) 79 if (!is(T == typeof(null))) { 80 81 // If there is a part to append to 82 if (textParts.length) { 83 84 textParts[$-1].text ~= text; 85 86 } 87 88 // Nope, make a new part 89 else textParts ~= Part(rebindable(cast(const Style) null), text); 90 91 } 92 93 /// Push text to the label. 94 /// Params: 95 /// style = Style of the text. 96 /// text = Text to add. 97 void push(const Style style, string text) { 98 99 textParts ~= Part(style.rebindable, text); 100 101 } 102 103 /// Ditto. 104 void push(string text) { 105 106 this ~= text; 107 108 } 109 110 /// Get the current text of the label, as plain text. 111 string text() const { 112 113 return textParts 114 .map!"a.text" 115 .join; 116 117 } 118 119 /// Erase all label contents. 120 void clear() { 121 122 textParts = []; 123 124 } 125 126 protected override void resizeImpl(Vector2 available) { 127 128 minSize = style.measureText(available, text); 129 130 } 131 132 protected override void drawImpl(Rectangle outer, Rectangle inner) { 133 134 const style = pickStyle(); 135 style.drawBackground(outer); 136 137 /// Current position on the screen to append to. 138 auto cursor = Vector2(inner.x, inner.y); 139 140 foreach (part; textParts) { 141 142 auto text = part.text; 143 144 while (text.length) { 145 146 auto current = text.until("\n", No.openRight).to!string; 147 text = text[current.length .. $]; 148 149 // Get area to draw in 150 auto thisStyle = part.style is null ? style : part.style; 151 auto area = thisStyle.measureText( 152 Rectangle(cursor.x, cursor.y, inner.w, inner.h), 153 current 154 ); 155 // TODO: wrapping+indent 156 157 thisStyle.drawBackground(area); 158 thisStyle.drawText(area, current); 159 160 // Move the cursor 161 cursor.y += area.h - thisStyle.fontSize * thisStyle.lineHeight; 162 163 // Ended with a newline 164 if (current[$-1] == '\n') cursor.x = inner.x; 165 else cursor.x += area.w; 166 167 } 168 169 } 170 171 } 172 173 protected override const(Style) pickStyle() const { 174 175 return style; 176 177 } 178 179 }