veloren_voxygen/ui/widgets/
rich_text.rs1use crate::hud::{controller_icons as icon_utils, img_ids::Imgs};
2use conrod_core::{
3 Color, FontSize, Positionable, Sizeable, Ui, Widget, WidgetCommon, builder_methods, image,
4 position::Dimension, text, widget, widget_ids,
5};
6use regex::Regex;
7use std::sync::LazyLock;
8
9enum TextSegment<'a> {
11 Text(&'a str),
12 Image(image::Id), Newline,
14}
15
16pub struct State {
17 ids: Ids,
18}
19
20widget_ids! {
21 struct Ids {
22 text_ids[],
23 image_ids[],
24 }
25}
26
27#[derive(WidgetCommon)]
32pub struct RichText<'a> {
33 #[conrod(common_builder)]
34 common: widget::CommonBuilder,
35 segments: Vec<TextSegment<'a>>,
36 text_segments: usize,
37 image_segments: usize,
38 style: widget::text::Style,
39 font_id: Option<text::font::Id>,
40 line_spacing: f64,
41}
42
43impl<'a> RichText<'a> {
44 builder_methods! {
45 pub color { style.color = Some(Color) }
46 pub font_size { style.font_size = Some(FontSize) }
47 pub font_id { font_id = Some(text::font::Id) }
48 pub line_spacing { line_spacing = f64 }
49 pub justify { style.justify = Some(text::Justify) }
50 }
51
52 pub fn new(string: &'a str, imgs: &'a Imgs) -> Self {
59 let (segments, text_segments, image_segments) = Self::parse(string, imgs);
60
61 RichText {
62 common: widget::CommonBuilder::default(),
63 segments,
64 text_segments,
65 image_segments,
66 style: widget::text::Style::default(),
67 font_id: None,
68 line_spacing: 5.0,
69 }
70 }
71
72 fn parse(input: &'a str, imgs: &Imgs) -> (Vec<TextSegment<'a>>, usize, usize) {
75 static RE: LazyLock<Regex> =
77 LazyLock::new(|| Regex::new(r":(?P<tag>[^:\s]+):").expect("Invalid Regex"));
78 let mut segments = Vec::new();
79 let mut text_segments = 0;
80 let mut image_segments = 0;
81 let mut last_end = 0;
82
83 for check in RE.captures_iter(input) {
84 let whole = check.get(0).unwrap();
85 let tag = &check["tag"];
86
87 if whole.start() > last_end {
89 let text_strip = &input[last_end..whole.start()];
90 for (i, line) in text_strip.split('\n').enumerate() {
92 if i > 0 {
93 segments.push(TextSegment::Newline);
94 }
95 if !line.is_empty() {
96 segments.push(TextSegment::Text(line));
97 text_segments += 1;
98 }
99 }
100 }
101
102 let img_id = icon_utils::get_controller_icon_id_from_string(tag, imgs);
104
105 segments.push(TextSegment::Image(img_id));
106 image_segments += 1;
107
108 last_end = whole.end();
109 }
110
111 if last_end < input.len() {
113 let trailing = &input[last_end..];
114 for (i, line) in trailing.split('\n').enumerate() {
115 if i > 0 {
116 segments.push(TextSegment::Newline);
117 }
118 if !line.is_empty() {
119 segments.push(TextSegment::Text(line));
120 text_segments += 1;
121 }
122 }
123 }
124
125 (segments, text_segments, image_segments)
126 }
127
128 fn calculate_dimensions(&self, ui: &Ui) -> [f64; 2] {
130 let font_id = self.font_id.or(ui.theme.font_id).expect("No font provided");
131 let font = ui.fonts.get(font_id).expect("Font not found");
132 let font_size = self.style.font_size(&ui.theme);
133 let line_height = font_size as f64 + self.line_spacing;
134
135 let mut max_w: f64 = 0.0;
136 let mut current_w: f64 = 0.0;
137 let mut total_h: f64 = line_height;
138
139 for segment in &self.segments {
140 match segment {
141 TextSegment::Text(s) => {
142 current_w += text::line::width(s, font, font_size);
143 },
144 TextSegment::Image(_) => {
145 current_w += font_size as f64;
146 },
147 TextSegment::Newline => {
148 max_w = max_w.max(current_w);
149 current_w = 0.0;
150 total_h += line_height;
151 },
152 }
153 }
154 [max_w.max(current_w), total_h]
155 }
156}
157
158impl<'a> Widget for RichText<'a> {
159 type Event = ();
160 type State = State;
161 type Style = ();
162
163 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
164 State {
165 ids: Ids::new(id_gen),
166 }
167 }
168
169 fn style(&self) -> Self::Style {}
170
171 fn default_x_dimension(&self, ui: &Ui) -> Dimension {
172 let [w, _h] = self.calculate_dimensions(ui);
173 Dimension::Absolute(w)
174 }
175
176 fn default_y_dimension(&self, ui: &Ui) -> Dimension {
177 let [_w, h] = self.calculate_dimensions(ui);
178 Dimension::Absolute(h)
179 }
180
181 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
182 let widget::UpdateArgs {
183 id,
184 state,
185 ui,
186 rect,
187 ..
188 } = args;
189
190 let font_size = self.style.font_size(ui.theme());
191 let line_height = font_size as f64 + self.line_spacing;
192 let font_id = self.font_id.or(ui.theme.font_id).expect("No font provided");
193
194 let available_width = rect.w();
195 let justify = self.style.justify(ui.theme());
196
197 let line_widths: Vec<f64> = {
199 let font = ui.fonts.get(font_id).expect("Font not found"); let mut current_width = 0.0;
201 let mut widths = Vec::new();
202
203 for segment in &self.segments {
204 match segment {
205 TextSegment::Text(s) => {
206 current_width += text::line::width(s, font, font_size);
207 },
208 TextSegment::Image(_) => {
209 current_width += font_size as f64;
210 },
211 TextSegment::Newline => {
212 widths.push(current_width);
213 current_width = 0.0;
214 },
215 }
216 }
217
218 widths.push(current_width);
219 widths
220 };
221
222 state.update(|s| {
224 s.ids
225 .text_ids
226 .resize(self.text_segments, &mut ui.widget_id_generator());
227 s.ids
228 .image_ids
229 .resize(self.image_segments, &mut ui.widget_id_generator());
230 });
231
232 let mut y_cursor = 0.0;
233 let mut text_idx = 0;
234 let mut image_idx = 0;
235 let mut line_idx = 0;
236
237 let mut x_cursor = match justify {
238 text::Justify::Left => 0.0,
239 text::Justify::Right => available_width - line_widths[line_idx],
240 text::Justify::Center => (available_width - line_widths[line_idx]) / 2.0,
241 };
242
243 for segment in &self.segments {
244 match segment {
245 TextSegment::Text(string) => {
246 if string.is_empty() {
247 continue;
248 }
249
250 let text_width = {
251 let font = ui.fonts.get(font_id).expect("Font not found");
252 text::line::width(string, font, font_size)
253 };
254
255 widget::Text::new(string)
256 .with_style(self.style)
257 .font_id(font_id)
258 .parent(id)
259 .graphics_for(id)
260 .top_left_with_margins_on(id, y_cursor, x_cursor)
261 .set(state.ids.text_ids[text_idx], ui);
262
263 x_cursor += text_width;
264 text_idx += 1;
265 },
266 TextSegment::Image(image_id) => {
267 let image_size = font_size as f64;
268 let v_offset = 1.5;
271
272 let color = self.style.color.unwrap_or(Color::Rgba(1.0, 1.0, 1.0, 1.0));
275 widget::Image::new(*image_id)
276 .wh([image_size, image_size])
277 .parent(id)
278 .graphics_for(id)
279 .color(Some(color))
280 .top_left_with_margins_on(id, y_cursor + v_offset, x_cursor)
281 .set(state.ids.image_ids[image_idx], ui);
282
283 x_cursor += image_size;
284 image_idx += 1;
285 },
286 TextSegment::Newline => {
287 line_idx += 1;
288 x_cursor = match justify {
289 text::Justify::Left => 0.0,
290 text::Justify::Right => available_width - line_widths[line_idx],
291 text::Justify::Center => (available_width - line_widths[line_idx]) / 2.0,
292 };
293 y_cursor += line_height;
294 },
295 }
296 }
297 }
298}