1use dotzuki_engine::render::painter::Painter;
11use dotzuki_engine::render::{Rgba, TilePos};
12
13use crate::layout_engine::types::{DataContext, LayoutElement, RenderContext, RenderError, TextAlign,
14 TextParams};
15
16pub fn render_text(
28 element: &LayoutElement,
29 params: &TextParams,
30 ctx: &DataContext,
31 render_ctx: &RenderContext,
32 painter: &mut dyn Painter,
33) -> Result<(), RenderError> {
34 let localized = params.value.get(ctx.lang());
38 let resolved_text = ctx.resolve(localized);
39
40 let rect = &element.rect;
42 let tile_width = rect.tw.unwrap_or(20);
43 let tile_height = rect.th.unwrap_or(18);
44 let base_tx = rect.tx.resolve(ctx);
45 let base_ty = rect.ty.resolve(ctx);
46 let align = params.align.as_ref().unwrap_or(&TextAlign::Left);
47 let wrap = params.wrap.as_deref() == Some("word");
48
49 let theme = render_ctx.theme;
54 if theme.proportional(painter.supports_proportional()) {
55 let color = params
56 .color
57 .as_deref()
58 .map(parse_color)
59 .unwrap_or_else(|| theme.ink_color());
60 let base_px = base_tx * 8;
61 let base_py = base_ty * 8;
62 let width_px = tile_width * 8;
63 let height_px = tile_height * 8;
64 let scale = params.scale.unwrap_or(1).max(1);
67 let line_h = (crate::embedded_font::GLYPH_SIZE + 3) * scale
69 + params.line_spacing.unwrap_or(0) as u32;
70 let lines = if wrap {
71 word_wrap_px(&resolved_text, width_px, &*painter)
72 } else {
73 resolved_text.split('\n').map(|l| l.to_string()).collect()
74 };
75 let mut y = base_py;
76 for line in &lines {
77 if y >= base_py + height_px {
78 break; }
80 let w = painter.measure_text_px_scaled(line, scale);
81 let off = match align {
82 TextAlign::Left => 0,
83 TextAlign::Center => width_px.saturating_sub(w) / 2,
84 TextAlign::Right => width_px.saturating_sub(w),
85 };
86 painter.draw_text_px_scaled(base_px + off, y, line, scale, color);
87 y += line_h;
88 }
89 return Ok(());
90 }
91
92 let color = params
94 .color
95 .as_deref()
96 .map(parse_color)
97 .unwrap_or(Rgba::INK_BLACK);
98 let max_chars = tile_width as usize;
99 let lines = if wrap {
100 word_wrap(&resolved_text, max_chars)
101 } else {
102 hard_break_lines(&resolved_text, max_chars)
103 };
104 let line_spacing = params.line_spacing.unwrap_or(0) as u32;
105 for (line_idx, line) in lines.iter().enumerate() {
106 let row = base_ty + line_idx as u32 * (1 + line_spacing);
107 if row >= base_ty + tile_height {
108 break; }
110
111 let text_width = line.chars().count().min(max_chars) as u32;
112 let offset_x = align_offset(text_width, tile_width, align);
113
114 for (char_idx, ch) in line.chars().enumerate() {
115 if char_idx >= max_chars {
116 break;
117 }
118 let col = base_tx + offset_x + char_idx as u32;
119 painter.draw_glyph(TilePos::new(col, row), ch, color);
120 }
121 }
122
123 Ok(())
124}
125
126pub fn parse_color(s: &str) -> Rgba {
135 if let Some(hex) = s.strip_prefix('#') {
136 if let Some(c) = parse_hex_color(hex) {
137 return c;
138 }
139 }
140 match s.to_lowercase().as_str() {
141 "black" => Rgba::INK_BLACK,
142 "darkgray" | "dark_gray" => Rgba::INK_DARK_GRAY,
143 "lightgray" | "light_gray" => Rgba::INK_LIGHT_GRAY,
144 "white" => Rgba::INK_WHITE,
145 _ => Rgba::INK_BLACK,
146 }
147}
148
149fn parse_hex_color(hex: &str) -> Option<Rgba> {
150 let nibble = |c: u8| (c as char).to_digit(16).map(|d| d as u8);
151 let byte = |hi: u8, lo: u8| Some(nibble(hi)? * 16 + nibble(lo)?);
152 let b = hex.as_bytes();
153 match b.len() {
154 3 => Some(Rgba::rgb(
155 byte(b[0], b[0])?,
156 byte(b[1], b[1])?,
157 byte(b[2], b[2])?,
158 )),
159 6 => Some(Rgba::rgb(byte(b[0], b[1])?, byte(b[2], b[3])?, byte(b[4], b[5])?)),
160 8 => Some(Rgba::new(
161 byte(b[0], b[1])?,
162 byte(b[2], b[3])?,
163 byte(b[4], b[5])?,
164 byte(b[6], b[7])?,
165 )),
166 _ => None,
167 }
168}
169
170fn hard_break_lines(text: &str, max_chars: usize) -> Vec<String> {
172 text.lines()
173 .map(|line| {
174 if line.chars().count() > max_chars {
175 line.chars().take(max_chars).collect()
176 } else {
177 line.to_string()
178 }
179 })
180 .collect()
181}
182
183pub fn word_wrap(text: &str, max_chars: usize) -> Vec<String> {
187 let mut lines: Vec<String> = Vec::new();
188 let mut current = String::new();
189
190 for word in text.split_whitespace() {
191 if current.is_empty() {
192 if word.chars().count() > max_chars {
193 for chunk in chunk_str(word, max_chars) {
195 lines.push(chunk.to_string());
196 }
197 } else {
198 current = word.to_string();
199 }
200 } else if current.chars().count() + 1 + word.chars().count() <= max_chars {
201 current.push(' ');
202 current.push_str(word);
203 } else {
204 lines.push(std::mem::take(&mut current));
205 if word.chars().count() > max_chars {
206 for chunk in chunk_str(word, max_chars) {
207 lines.push(chunk.to_string());
208 }
209 } else {
210 current = word.to_string();
211 }
212 }
213 }
214
215 if !current.is_empty() {
216 lines.push(current);
217 }
218
219 if lines.is_empty() {
220 lines.push(String::new());
221 }
222
223 lines
224}
225
226pub fn word_wrap_px(text: &str, max_px: u32, painter: &dyn Painter) -> Vec<String> {
231 let max_px = max_px.max(1);
232 let space_px = painter.measure_text_px(" ");
233 let mut lines: Vec<String> = Vec::new();
234
235 for raw in text.split('\n') {
236 let mut current = String::new();
237 let mut cur_px = 0u32;
238 for word in raw.split(' ') {
239 if word.is_empty() {
240 continue;
241 }
242 let word_px = painter.measure_text_px(word);
243 if word_px <= max_px {
244 let sep = if current.is_empty() { 0 } else { space_px };
245 if cur_px + sep + word_px <= max_px {
246 if !current.is_empty() {
247 current.push(' ');
248 cur_px += sep;
249 }
250 current.push_str(word);
251 cur_px += word_px;
252 } else {
253 lines.push(std::mem::take(&mut current));
254 current.push_str(word);
255 cur_px = word_px;
256 }
257 } else {
258 if !current.is_empty() {
260 lines.push(std::mem::take(&mut current));
261 cur_px = 0;
262 }
263 for ch in word.chars() {
264 let ch_px = painter.measure_text_px(ch.encode_utf8(&mut [0u8; 4]));
265 if !current.is_empty() && cur_px + ch_px > max_px {
266 lines.push(std::mem::take(&mut current));
267 cur_px = 0;
268 }
269 current.push(ch);
270 cur_px += ch_px;
271 }
272 }
273 }
274 lines.push(current); }
276
277 if lines.is_empty() {
278 lines.push(String::new());
279 }
280 lines
281}
282
283fn chunk_str(s: &str, max_chars: usize) -> Vec<&str> {
285 let mut chunks = Vec::new();
286 let mut remaining = s;
287 while !remaining.is_empty() {
288 let end = remaining
289 .char_indices()
290 .take(max_chars)
291 .last()
292 .map(|(i, c)| i + c.len_utf8())
293 .unwrap_or(remaining.len());
294 chunks.push(&remaining[..end]);
295 remaining = &remaining[end..];
296 }
297 chunks
298}
299
300pub fn align_offset(text_width: u32, available_width: u32, align: &TextAlign) -> u32 {
306 match align {
307 TextAlign::Left => 0,
308 TextAlign::Center => {
309 if text_width >= available_width {
310 0
311 } else {
312 (available_width - text_width) / 2
313 }
314 }
315 TextAlign::Right => available_width.saturating_sub(text_width),
316 }
317}
318
319#[cfg(test)]
322mod tests {
323 use super::*;
324 use crate::layout_engine::types::{Coord, ElementParams, ElementRect};
325 use dotzuki_engine::render::Rgba as EngineRgba;
326
327 #[derive(Debug, Default)]
330 struct RecordingPainter {
331 glyphs: Vec<(TilePos, char, EngineRgba)>,
332 texts: Vec<(TilePos, String, EngineRgba)>,
333 }
334
335 impl RecordingPainter {
336 fn new() -> Self {
337 Self::default()
338 }
339
340 fn glyph_at(&self, tx: u32, ty: u32) -> Option<char> {
341 self.glyphs
342 .iter()
343 .find(|(pos, _, _)| pos.tx == tx && pos.ty == ty)
344 .map(|(_, ch, _)| *ch)
345 }
346 }
347
348 impl Painter for RecordingPainter {
349 fn clear(&mut self, _color: EngineRgba) {}
350
351 fn draw_text_box(
352 &mut self,
353 _rect: dotzuki_engine::render::TileRect,
354 _color: EngineRgba,
355 ) {
356 }
357
358 fn draw_text(&mut self, pos: TilePos, text: &str, color: EngineRgba) {
359 self.texts.push((pos, text.to_string(), color));
360 }
361
362 fn draw_glyph(&mut self, pos: TilePos, glyph: char, color: EngineRgba) {
363 self.glyphs.push((pos, glyph, color));
364 }
365
366 fn draw_pixel_rect(
367 &mut self,
368 _px: u32,
369 _py: u32,
370 _pw: u32,
371 _ph: u32,
372 _color: EngineRgba,
373 ) {
374 }
375
376 fn draw_gb_tile(
377 &mut self,
378 _pos: TilePos,
379 _tile_id: u8,
380 _fallback: &str,
381 _color: EngineRgba,
382 ) {
383 }
384 }
385
386 fn make_element(value: &str, tx: u32, ty: u32, tw: u32, th: u32) -> LayoutElement {
389 LayoutElement {
390 id: String::new(),
391 element_type: "text".to_string(),
392 rect: ElementRect {
393 tx: Coord::Literal(tx),
394 ty: Coord::Literal(ty),
395 tw: Some(tw),
396 th: Some(th),
397 },
398 visible: crate::layout_engine::types::Visibility::Static(true),
399 z_index: 0,
400 params: ElementParams::Text(TextParams {
401 value: value.into(),
402 format: None,
403 color: None,
404 align: None,
405 font: None,
406 wrap: None,
407 line_spacing: None,
408 scale: None,
409 }),
410 }
411 }
412
413 fn make_theme() -> crate::layout_engine::types::Theme {
414 Default::default()
415 }
416
417 fn render_ctx<'a>(
418 theme: &'a crate::layout_engine::types::Theme,
419 fonts: &'a std::collections::HashMap<String, ()>,
420 tilesets: &'a std::collections::HashMap<String, ()>,
421 ) -> RenderContext<'a> {
422 RenderContext {
423 screen: "test",
424 theme,
425 fonts,
426 tilesets,
427 images: crate::layout_engine::types::empty_image_registry(),
428 }
429 }
430
431 #[test]
434 fn parse_color_black() {
435 assert_eq!(parse_color("black"), Rgba::INK_BLACK);
436 }
437
438 #[test]
439 fn parse_color_darkgray_variants() {
440 assert_eq!(parse_color("darkgray"), Rgba::INK_DARK_GRAY);
441 assert_eq!(parse_color("dark_gray"), Rgba::INK_DARK_GRAY);
442 }
443
444 #[test]
445 fn parse_color_lightgray_variants() {
446 assert_eq!(parse_color("lightgray"), Rgba::INK_LIGHT_GRAY);
447 assert_eq!(parse_color("light_gray"), Rgba::INK_LIGHT_GRAY);
448 }
449
450 #[test]
451 fn parse_color_white() {
452 assert_eq!(parse_color("white"), Rgba::INK_WHITE);
453 }
454
455 #[test]
456 fn parse_color_case_insensitive() {
457 assert_eq!(parse_color("BLACK"), Rgba::INK_BLACK);
458 assert_eq!(parse_color("White"), Rgba::INK_WHITE);
459 }
460
461 #[test]
462 fn parse_color_unknown_returns_black() {
463 assert_eq!(parse_color("red"), Rgba::INK_BLACK);
464 assert_eq!(parse_color(""), Rgba::INK_BLACK);
465 }
466
467 #[test]
470 fn word_wrap_short_text() {
471 assert_eq!(word_wrap("hello", 10), vec!["hello"]);
472 }
473
474 #[test]
475 fn word_wrap_splits_at_space() {
476 let lines = word_wrap("hello world test", 10);
477 assert_eq!(lines, vec!["hello", "world test"]);
478 }
479
480 #[test]
481 fn word_wrap_exact_fit() {
482 assert_eq!(word_wrap("12345 12345", 5), vec!["12345", "12345"]);
483 }
484
485 #[test]
486 fn word_wrap_long_word_breaks() {
487 let lines = word_wrap("supercalifragilistic", 5);
488 for line in &lines {
489 assert!(line.chars().count() <= 5, "{:?} too long", line);
490 }
491 assert!(lines.len() > 1);
492 }
493
494 #[test]
495 fn word_wrap_empty_returns_empty_line() {
496 assert_eq!(word_wrap("", 10), vec![""]);
497 }
498
499 #[test]
500 fn word_wrap_preserves_spaces_in_result() {
501 let lines = word_wrap("a b c", 3);
502 assert_eq!(lines, vec!["a b", "c"]);
504 }
505
506 #[test]
509 fn align_left_is_zero() {
510 assert_eq!(align_offset(5, 20, &TextAlign::Left), 0);
511 }
512
513 #[test]
514 fn align_center() {
515 assert_eq!(align_offset(5, 20, &TextAlign::Center), 7); assert_eq!(align_offset(4, 20, &TextAlign::Center), 8); }
518
519 #[test]
520 fn align_center_clamped() {
521 assert_eq!(align_offset(25, 20, &TextAlign::Center), 0);
522 }
523
524 #[test]
525 fn align_right() {
526 assert_eq!(align_offset(5, 20, &TextAlign::Right), 15); assert_eq!(align_offset(25, 20, &TextAlign::Right), 0); }
529
530 #[test]
533 fn render_simple_text() {
534 let elem = make_element("Hello", 2, 3, 20, 18);
535 let params = match &elem.params {
536 ElementParams::Text(p) => p,
537 _ => unreachable!(),
538 };
539 let ctx = DataContext::new();
540 let theme = Default::default();
541 let fonts = std::collections::HashMap::new();
542 let tilesets = std::collections::HashMap::new();
543 let rc = render_ctx(&theme, &fonts, &tilesets);
544 let mut p = RecordingPainter::new();
545
546 render_text(&elem, params, &ctx, &rc, &mut p).unwrap();
547
548 assert_eq!(p.glyphs.len(), 5);
549 assert_eq!(p.glyph_at(2, 3), Some('H'));
550 assert_eq!(p.glyph_at(3, 3), Some('e'));
551 assert_eq!(p.glyph_at(6, 3), Some('o'));
552 }
553
554 #[test]
555 fn render_template_resolution() {
556 let elem = make_element("{name} Lv{level}", 0, 0, 20, 18);
557 let params = match &elem.params {
558 ElementParams::Text(p) => p,
559 _ => unreachable!(),
560 };
561 let mut ctx = DataContext::new();
562 ctx.set("name", "SPARKIT");
563 ctx.set("level", 25i64);
564 let theme = make_theme();
565 let fonts: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
566 let tilesets: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
567 let rc = render_ctx(&theme, &fonts, &tilesets);
568 let mut p = RecordingPainter::new();
569
570 render_text(&elem, params, &ctx, &rc, &mut p).unwrap();
571
572 let rendered: String = p.glyphs.iter().map(|(_, ch, _)| *ch).collect();
574 assert_eq!(rendered, "SPARKIT Lv25");
575 }
576
577 #[test]
578 fn localized_value_get_selects_and_falls_back() {
579 use crate::layout_engine::types::LocalizedValue;
580 let mut m = std::collections::BTreeMap::new();
581 m.insert("en".to_string(), "YES".to_string());
582 m.insert("zh".to_string(), "是".to_string());
583 let lv = LocalizedValue::Localized(m);
584 assert_eq!(lv.get("zh"), "是");
585 assert_eq!(lv.get("en"), "YES");
586 assert_eq!(lv.get("ja"), "YES");
588 assert_eq!(LocalizedValue::Plain("HI".into()).get("zh"), "HI");
590 }
591
592 #[test]
593 fn render_localized_text_picks_active_language() {
594 use crate::layout_engine::types::LocalizedValue;
595 let mut elem = make_element("placeholder", 0, 0, 20, 18);
596 if let ElementParams::Text(ref mut tp) = elem.params {
597 let mut m = std::collections::BTreeMap::new();
598 m.insert("en".to_string(), "YES".to_string());
599 m.insert("zh".to_string(), "是".to_string());
600 tp.value = LocalizedValue::Localized(m);
601 }
602 let params = match &elem.params {
603 ElementParams::Text(p) => p,
604 _ => unreachable!(),
605 };
606 let theme = make_theme();
607 let fonts: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
608 let tilesets: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
609 let rc = render_ctx(&theme, &fonts, &tilesets);
610
611 let mut ctx = DataContext::new();
613 ctx.set("__lang", "zh");
614 let mut p = RecordingPainter::new();
615 render_text(&elem, params, &ctx, &rc, &mut p).unwrap();
616 let zh: String = p.glyphs.iter().map(|(_, ch, _)| *ch).collect();
617 assert_eq!(zh, "是");
618
619 let ctx_default = DataContext::new();
621 let mut p2 = RecordingPainter::new();
622 render_text(&elem, params, &ctx_default, &rc, &mut p2).unwrap();
623 let en: String = p2.glyphs.iter().map(|(_, ch, _)| *ch).collect();
624 assert_eq!(en, "YES");
625 }
626
627 #[test]
628 fn render_center_aligned() {
629 let mut elem = make_element("AB", 0, 0, 10, 18);
630 if let ElementParams::Text(ref mut tp) = elem.params {
631 tp.align = Some(TextAlign::Center);
632 }
633 let params = match &elem.params {
634 ElementParams::Text(p) => p,
635 _ => unreachable!(),
636 };
637 let ctx = DataContext::new();
638 let theme = make_theme();
639 let fonts: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
640 let tilesets: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
641 let rc = render_ctx(&theme, &fonts, &tilesets);
642 let mut p = RecordingPainter::new();
643
644 render_text(&elem, params, &ctx, &rc, &mut p).unwrap();
645
646 assert_eq!(p.glyph_at(4, 0), Some('A'));
648 assert_eq!(p.glyph_at(5, 0), Some('B'));
649 }
650
651 #[test]
652 fn render_right_aligned() {
653 let mut elem = make_element("X", 0, 0, 5, 18);
654 if let ElementParams::Text(ref mut tp) = elem.params {
655 tp.align = Some(TextAlign::Right);
656 }
657 let params = match &elem.params {
658 ElementParams::Text(p) => p,
659 _ => unreachable!(),
660 };
661 let ctx = DataContext::new();
662 let theme = make_theme();
663 let fonts: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
664 let tilesets: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
665 let rc = render_ctx(&theme, &fonts, &tilesets);
666 let mut p = RecordingPainter::new();
667
668 render_text(&elem, params, &ctx, &rc, &mut p).unwrap();
669
670 assert_eq!(p.glyph_at(4, 0), Some('X'));
672 }
673
674 #[test]
675 fn render_word_wrap() {
676 let mut elem = make_element("hello world foo bar baz", 0, 0, 6, 18);
677 if let ElementParams::Text(ref mut tp) = elem.params {
678 tp.wrap = Some("word".to_string());
679 }
680 let params = match &elem.params {
681 ElementParams::Text(p) => p,
682 _ => unreachable!(),
683 };
684 let ctx = DataContext::new();
685 let theme = make_theme();
686 let fonts: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
687 let tilesets: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
688 let rc = render_ctx(&theme, &fonts, &tilesets);
689 let mut p = RecordingPainter::new();
690
691 render_text(&elem, params, &ctx, &rc, &mut p).unwrap();
692
693 let has_row_0 = p.glyphs.iter().any(|(pos, _, _)| pos.ty == 0);
695 let has_row_1 = p.glyphs.iter().any(|(pos, _, _)| pos.ty == 1);
696 let has_row_2 = p.glyphs.iter().any(|(pos, _, _)| pos.ty == 2);
697 assert!(has_row_0);
698 assert!(has_row_1);
699 assert!(has_row_2);
700 }
701
702 #[test]
703 fn render_color_from_params() {
704 let mut elem = make_element("Hi", 0, 0, 20, 18);
705 if let ElementParams::Text(ref mut tp) = elem.params {
706 tp.color = Some("white".to_string());
707 }
708 let params = match &elem.params {
709 ElementParams::Text(p) => p,
710 _ => unreachable!(),
711 };
712 let ctx = DataContext::new();
713 let theme = make_theme();
714 let fonts: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
715 let tilesets: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
716 let rc = render_ctx(&theme, &fonts, &tilesets);
717 let mut p = RecordingPainter::new();
718
719 render_text(&elem, params, &ctx, &rc, &mut p).unwrap();
720
721 assert!(!p.glyphs.is_empty());
722 assert_eq!(p.glyphs[0].2, Rgba::INK_WHITE);
723 }
724
725 #[test]
726 fn render_clips_to_rect_height() {
727 let elem = make_element("A\nB\nC\nD\nE", 0, 0, 20, 2);
728 let params = match &elem.params {
729 ElementParams::Text(p) => p,
730 _ => unreachable!(),
731 };
732 let ctx = DataContext::new();
733 let theme = make_theme();
734 let fonts: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
735 let tilesets: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
736 let rc = render_ctx(&theme, &fonts, &tilesets);
737 let mut p = RecordingPainter::new();
738
739 render_text(&elem, params, &ctx, &rc, &mut p).unwrap();
740
741 let max_ty = p.glyphs.iter().map(|(pos, _, _)| pos.ty).max().unwrap_or(0);
743 assert!(max_ty < 2, "expected rows < 2, got max_ty={}", max_ty);
744 }
745
746 #[test]
747 fn render_font_config() {
748 let mut elem = make_element("OK", 0, 0, 20, 18);
751 if let ElementParams::Text(ref mut tp) = elem.params {
752 tp.font = Some("battle".to_string());
753 }
754 let params = match &elem.params {
755 ElementParams::Text(p) => p,
756 _ => unreachable!(),
757 };
758 let ctx = DataContext::new();
759 let theme = make_theme();
760 let fonts: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
761 let tilesets: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
762 let rc = render_ctx(&theme, &fonts, &tilesets);
763 let mut p = RecordingPainter::new();
764
765 let result = render_text(&elem, params, &ctx, &rc, &mut p);
766 assert!(result.is_ok());
767 let rendered: String = p.glyphs.iter().map(|(_, ch, _)| *ch).collect();
768 assert_eq!(rendered, "OK");
769 }
770
771 #[test]
772 fn render_line_spacing() {
773 let mut elem = make_element("A\nB", 0, 0, 20, 18);
774 if let ElementParams::Text(ref mut tp) = elem.params {
775 tp.line_spacing = Some(2); }
777 let params = match &elem.params {
778 ElementParams::Text(p) => p,
779 _ => unreachable!(),
780 };
781 let ctx = DataContext::new();
782 let theme = make_theme();
783 let fonts: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
784 let tilesets: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
785 let rc = render_ctx(&theme, &fonts, &tilesets);
786 let mut p = RecordingPainter::new();
787
788 render_text(&elem, params, &ctx, &rc, &mut p).unwrap();
789
790 assert_eq!(p.glyph_at(0, 0), Some('A'));
792 assert_eq!(p.glyph_at(0, 3), Some('B'));
793 }
794
795 #[test]
798 fn chunk_str_simple() {
799 assert_eq!(chunk_str("abc", 2), vec!["ab", "c"]);
800 }
801
802 #[test]
803 fn chunk_str_exact_multiple() {
804 assert_eq!(chunk_str("abcd", 2), vec!["ab", "cd"]);
805 }
806
807 #[test]
808 fn chunk_str_empty() {
809 let v: Vec<&str> = Vec::new();
810 assert_eq!(chunk_str("", 5), v);
811 }
812}