thomas/systems/
sys_terminal_ui_renderer.rs

1use std::{collections::HashMap, rc::Rc};
2
3use crate::{
4    Alignment, GameCommand, GameCommandsArg, IntCoords2d, Layer, Query, QueryResultList, Rgb,
5    System, SystemsGenerator, TerminalCamera, TerminalRenderer, TerminalTextCharacter,
6    TerminalTransform, Text, UiAnchor, WorldText, EVENT_UPDATE,
7};
8
9/// A generator responsible for setting up and performing UI rendering in a terminal game. This systems generator is
10/// included for you, you don't need to include it.
11pub struct TerminalUiRendererSystemsGenerator {}
12impl TerminalUiRendererSystemsGenerator {
13    pub fn new() -> Self {
14        Self {}
15    }
16}
17impl SystemsGenerator for TerminalUiRendererSystemsGenerator {
18    fn generate(&self) -> Vec<(&'static str, System)> {
19        vec![(
20            EVENT_UPDATE,
21            System::new(
22                vec![
23                    Query::new().has::<Text>(),
24                    Query::new().has::<WorldText>().has::<TerminalTransform>(),
25                    Query::new()
26                        .has::<TerminalTextCharacter>()
27                        .has::<TerminalRenderer>(),
28                    Query::new()
29                        .has_where::<TerminalCamera>(|cam| cam.is_main)
30                        .has::<TerminalTransform>(),
31                ],
32                update_text_ui,
33            ),
34        )]
35    }
36}
37
38fn update_text_ui(results: Vec<QueryResultList>, commands: GameCommandsArg) {
39    if let [text_results, world_text_results, drawn_text_results, main_cam_results, ..] =
40        &results[..]
41    {
42        let main_cam = main_cam_results.get_only::<TerminalCamera>();
43        let main_cam_transform = main_cam_results.get_only::<TerminalTransform>();
44
45        let anchor_positions = get_anchor_positions(&main_cam, &main_cam_transform);
46
47        wipe_existing_text(drawn_text_results, Rc::clone(&commands));
48
49        for text_result in text_results {
50            let text = text_result.components().get::<Text>();
51
52            let (anchor_x, anchor_y) = anchor_positions
53                .get(&text.anchor)
54                .expect("The anchor position can be determined.")
55                .values();
56
57            let chars = text.value.chars().collect::<Vec<char>>();
58
59            let justification_offset = get_justification_offset(&text.justification, chars.len());
60
61            let starting_position =
62                IntCoords2d::new(anchor_x, anchor_y) + justification_offset + text.offset;
63
64            add_text_entities(
65                &chars,
66                &starting_position,
67                &text.foreground_color,
68                &text.background_color,
69                Rc::clone(&commands),
70            );
71        }
72
73        for world_text_result in world_text_results {
74            let world_text = world_text_result.components().get::<WorldText>();
75            let world_text_transform = world_text_result.components().get::<TerminalTransform>();
76
77            let chars = world_text.value.chars().collect::<Vec<char>>();
78
79            let justification_offset =
80                get_justification_offset(&world_text.justification, chars.len());
81
82            let starting_position =
83                world_text_transform.coords + justification_offset + world_text.offset;
84
85            add_text_entities(
86                &chars,
87                &starting_position,
88                &world_text.foreground_color,
89                &world_text.background_color,
90                Rc::clone(&commands),
91            );
92        }
93    }
94}
95
96fn wipe_existing_text(text_character_query_results: &QueryResultList, commands: GameCommandsArg) {
97    for text_character in text_character_query_results {
98        commands
99            .borrow_mut()
100            .issue(GameCommand::DestroyEntity(*text_character.entity()));
101    }
102}
103
104fn get_anchor_positions(
105    main_camera: &TerminalCamera,
106    main_camera_transform: &TerminalTransform,
107) -> HashMap<UiAnchor, IntCoords2d> {
108    let (zero_indexed_width, zero_indexed_height) = (
109        main_camera.field_of_view.width() as i64 - 1,
110        main_camera.field_of_view.height() as i64 - 1,
111    );
112
113    let base_coords = main_camera_transform.coords;
114
115    HashMap::from([
116        (UiAnchor::TopLeft, base_coords),
117        (
118            UiAnchor::MiddleTop,
119            base_coords + IntCoords2d::new(zero_indexed_width / 2, 0),
120        ),
121        (
122            UiAnchor::TopRight,
123            base_coords + IntCoords2d::new(zero_indexed_width, 0),
124        ),
125        (
126            UiAnchor::MiddleRight,
127            base_coords + IntCoords2d::new(zero_indexed_width, zero_indexed_height / 2),
128        ),
129        (
130            UiAnchor::BottomRight,
131            base_coords + IntCoords2d::new(zero_indexed_width, zero_indexed_height),
132        ),
133        (
134            UiAnchor::MiddleBottom,
135            base_coords + IntCoords2d::new(zero_indexed_width / 2, zero_indexed_height),
136        ),
137        (
138            UiAnchor::BottomLeft,
139            base_coords + IntCoords2d::new(0, zero_indexed_height),
140        ),
141        (
142            UiAnchor::MiddleLeft,
143            base_coords + IntCoords2d::new(0, zero_indexed_height / 2),
144        ),
145        (
146            UiAnchor::Middle,
147            base_coords + IntCoords2d::new(zero_indexed_width / 2, zero_indexed_height / 2),
148        ),
149    ])
150}
151
152fn get_justification_offset(justification: &Alignment, text_length: usize) -> IntCoords2d {
153    match justification {
154        Alignment::Left => IntCoords2d::zero(),
155        Alignment::Middle => IntCoords2d::new(-((text_length / 2) as i64), 0),
156        Alignment::Right => IntCoords2d::new(-(text_length as i64), 0),
157    }
158}
159
160fn add_text_entities(
161    chars: &Vec<char>,
162    starting_position: &IntCoords2d,
163    foreground_color: &Option<Rgb>,
164    background_color: &Option<Rgb>,
165    commands: GameCommandsArg,
166) {
167    let mut offset = IntCoords2d::zero();
168    for character in chars {
169        commands.borrow_mut().issue(GameCommand::AddEntity(vec![
170            Box::new(TerminalTextCharacter {}),
171            Box::new(TerminalRenderer {
172                display: *character,
173                layer: Layer::below(&Layer::furthest_foreground()),
174                foreground_color: *foreground_color,
175                background_color: *background_color,
176            }),
177            Box::new(TerminalTransform {
178                coords: *starting_position + offset,
179            }),
180        ]));
181
182        offset += IntCoords2d::right();
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    mod test_update_text_ui {
191        use std::cell::RefCell;
192
193        use crate::{Dimensions2d, Entity, QueryResult, StoredComponentList};
194
195        use super::*;
196
197        mod text {
198            use super::*;
199
200            mod with_camera_at_origin {
201                use crate::{Component, GameCommandQueue};
202
203                use super::*;
204
205                fn make_basic_results() -> Vec<QueryResultList> {
206                    let results = vec![
207                        QueryResultList::new(vec![]),
208                        QueryResultList::new(vec![]),
209                        QueryResultList::new(vec![]),
210                        QueryResultList::new(vec![QueryResult::new(
211                            Entity(1),
212                            StoredComponentList::new(vec![
213                                Rc::new(RefCell::new(Box::new(TerminalCamera {
214                                    field_of_view: Dimensions2d::new(10, 10),
215                                    is_main: true,
216                                }))),
217                                Rc::new(RefCell::new(Box::new(TerminalTransform {
218                                    coords: IntCoords2d::zero(),
219                                }))),
220                            ]),
221                        )]),
222                    ];
223
224                    results
225                }
226
227                #[test]
228                fn pos_is_correct_for_top_left() {
229                    let mut results = make_basic_results();
230
231                    results[0].push(QueryResult::new(
232                        Entity(0),
233                        StoredComponentList::new(vec![Rc::new(RefCell::new(Box::new(Text {
234                            value: String::from("T"),
235                            anchor: UiAnchor::TopLeft,
236                            justification: Alignment::Left,
237                            offset: IntCoords2d::zero(),
238                            foreground_color: None,
239                            background_color: None,
240                        })))]),
241                    ));
242
243                    let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
244
245                    update_text_ui(results, Rc::clone(&commands));
246
247                    assert!(commands
248                        .borrow()
249                        .queue()
250                        .iter()
251                        .find(|c| match c {
252                            GameCommand::AddEntity(comps) => {
253                                comps
254                                    .iter()
255                                    .find(|comp| {
256                                        comp.component_name() == TerminalTransform::name()
257                                            && (TerminalTransform::cast(&***comp)).unwrap().coords
258                                                == IntCoords2d::new(0, 0)
259                                    })
260                                    .is_some()
261                            }
262                            _ => {
263                                false
264                            }
265                        })
266                        .is_some());
267                }
268
269                #[test]
270                fn pos_is_correct_for_middle_top() {
271                    let mut results = make_basic_results();
272
273                    results[0].push(QueryResult::new(
274                        Entity(0),
275                        StoredComponentList::new(vec![Rc::new(RefCell::new(Box::new(Text {
276                            value: String::from("T"),
277                            anchor: UiAnchor::MiddleTop,
278                            justification: Alignment::Left,
279                            offset: IntCoords2d::zero(),
280                            foreground_color: None,
281                            background_color: None,
282                        })))]),
283                    ));
284
285                    let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
286
287                    update_text_ui(results, Rc::clone(&commands));
288
289                    assert!(commands
290                        .borrow()
291                        .queue()
292                        .iter()
293                        .find(|c| match c {
294                            GameCommand::AddEntity(comps) => {
295                                comps
296                                    .iter()
297                                    .find(|comp| {
298                                        comp.component_name() == TerminalTransform::name()
299                                            && (TerminalTransform::cast(&***comp)).unwrap().coords
300                                                == IntCoords2d::new(4, 0)
301                                    })
302                                    .is_some()
303                            }
304                            _ => {
305                                false
306                            }
307                        })
308                        .is_some());
309                }
310
311                #[test]
312                fn pos_is_correct_for_top_right() {
313                    let mut results = make_basic_results();
314
315                    results[0].push(QueryResult::new(
316                        Entity(0),
317                        StoredComponentList::new(vec![Rc::new(RefCell::new(Box::new(Text {
318                            value: String::from("T"),
319                            anchor: UiAnchor::TopRight,
320                            justification: Alignment::Left,
321                            offset: IntCoords2d::zero(),
322                            foreground_color: None,
323                            background_color: None,
324                        })))]),
325                    ));
326
327                    let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
328
329                    update_text_ui(results, Rc::clone(&commands));
330
331                    assert!(commands
332                        .borrow()
333                        .queue()
334                        .iter()
335                        .find(|c| match c {
336                            GameCommand::AddEntity(comps) => {
337                                comps
338                                    .iter()
339                                    .find(|comp| {
340                                        comp.component_name() == TerminalTransform::name()
341                                            && (TerminalTransform::cast(&***comp)).unwrap().coords
342                                                == IntCoords2d::new(9, 0)
343                                    })
344                                    .is_some()
345                            }
346                            _ => {
347                                false
348                            }
349                        })
350                        .is_some());
351                }
352
353                #[test]
354                fn pos_is_correct_for_middle_right() {
355                    let mut results = make_basic_results();
356
357                    results[0].push(QueryResult::new(
358                        Entity(0),
359                        StoredComponentList::new(vec![Rc::new(RefCell::new(Box::new(Text {
360                            value: String::from("T"),
361                            anchor: UiAnchor::MiddleRight,
362                            justification: Alignment::Left,
363                            offset: IntCoords2d::zero(),
364                            foreground_color: None,
365                            background_color: None,
366                        })))]),
367                    ));
368
369                    let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
370
371                    update_text_ui(results, Rc::clone(&commands));
372
373                    assert!(commands
374                        .borrow()
375                        .queue()
376                        .iter()
377                        .find(|c| match c {
378                            GameCommand::AddEntity(comps) => {
379                                comps
380                                    .iter()
381                                    .find(|comp| {
382                                        comp.component_name() == TerminalTransform::name()
383                                            && (TerminalTransform::cast(&***comp)).unwrap().coords
384                                                == IntCoords2d::new(9, 4)
385                                    })
386                                    .is_some()
387                            }
388                            _ => {
389                                false
390                            }
391                        })
392                        .is_some());
393                }
394
395                #[test]
396                fn pos_is_correct_for_bottom_right() {
397                    let mut results = make_basic_results();
398
399                    results[0].push(QueryResult::new(
400                        Entity(0),
401                        StoredComponentList::new(vec![Rc::new(RefCell::new(Box::new(Text {
402                            value: String::from("T"),
403                            anchor: UiAnchor::BottomRight,
404                            justification: Alignment::Left,
405                            offset: IntCoords2d::zero(),
406                            foreground_color: None,
407                            background_color: None,
408                        })))]),
409                    ));
410
411                    let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
412
413                    update_text_ui(results, Rc::clone(&commands));
414
415                    assert!(commands
416                        .borrow()
417                        .queue()
418                        .iter()
419                        .find(|c| match c {
420                            GameCommand::AddEntity(comps) => {
421                                comps
422                                    .iter()
423                                    .find(|comp| {
424                                        comp.component_name() == TerminalTransform::name()
425                                            && (TerminalTransform::cast(&***comp)).unwrap().coords
426                                                == IntCoords2d::new(9, 9)
427                                    })
428                                    .is_some()
429                            }
430                            _ => {
431                                false
432                            }
433                        })
434                        .is_some());
435                }
436
437                #[test]
438                fn pos_is_correct_for_middle_bottom() {
439                    let mut results = make_basic_results();
440
441                    results[0].push(QueryResult::new(
442                        Entity(0),
443                        StoredComponentList::new(vec![Rc::new(RefCell::new(Box::new(Text {
444                            value: String::from("T"),
445                            anchor: UiAnchor::MiddleBottom,
446                            justification: Alignment::Left,
447                            offset: IntCoords2d::zero(),
448                            foreground_color: None,
449                            background_color: None,
450                        })))]),
451                    ));
452
453                    let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
454
455                    update_text_ui(results, Rc::clone(&commands));
456
457                    assert!(commands
458                        .borrow()
459                        .queue()
460                        .iter()
461                        .find(|c| match c {
462                            GameCommand::AddEntity(comps) => {
463                                comps
464                                    .iter()
465                                    .find(|comp| {
466                                        comp.component_name() == TerminalTransform::name()
467                                            && (TerminalTransform::cast(&***comp)).unwrap().coords
468                                                == IntCoords2d::new(4, 9)
469                                    })
470                                    .is_some()
471                            }
472                            _ => {
473                                false
474                            }
475                        })
476                        .is_some());
477                }
478
479                #[test]
480                fn pos_is_correct_for_bottom_left() {
481                    let mut results = make_basic_results();
482
483                    results[0].push(QueryResult::new(
484                        Entity(0),
485                        StoredComponentList::new(vec![Rc::new(RefCell::new(Box::new(Text {
486                            value: String::from("T"),
487                            anchor: UiAnchor::BottomLeft,
488                            justification: Alignment::Left,
489                            offset: IntCoords2d::zero(),
490                            foreground_color: None,
491                            background_color: None,
492                        })))]),
493                    ));
494
495                    let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
496
497                    update_text_ui(results, Rc::clone(&commands));
498
499                    assert!(commands
500                        .borrow()
501                        .queue()
502                        .iter()
503                        .find(|c| match c {
504                            GameCommand::AddEntity(comps) => {
505                                comps
506                                    .iter()
507                                    .find(|comp| {
508                                        comp.component_name() == TerminalTransform::name()
509                                            && (TerminalTransform::cast(&***comp)).unwrap().coords
510                                                == IntCoords2d::new(0, 9)
511                                    })
512                                    .is_some()
513                            }
514                            _ => {
515                                false
516                            }
517                        })
518                        .is_some());
519                }
520
521                #[test]
522                fn pos_is_correct_for_middle_left() {
523                    let mut results = make_basic_results();
524
525                    results[0].push(QueryResult::new(
526                        Entity(0),
527                        StoredComponentList::new(vec![Rc::new(RefCell::new(Box::new(Text {
528                            value: String::from("T"),
529                            anchor: UiAnchor::MiddleLeft,
530                            justification: Alignment::Left,
531                            offset: IntCoords2d::zero(),
532                            foreground_color: None,
533                            background_color: None,
534                        })))]),
535                    ));
536
537                    let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
538
539                    update_text_ui(results, Rc::clone(&commands));
540
541                    assert!(commands
542                        .borrow()
543                        .queue()
544                        .iter()
545                        .find(|c| match c {
546                            GameCommand::AddEntity(comps) => {
547                                comps
548                                    .iter()
549                                    .find(|comp| {
550                                        comp.component_name() == TerminalTransform::name()
551                                            && (TerminalTransform::cast(&***comp)).unwrap().coords
552                                                == IntCoords2d::new(0, 4)
553                                    })
554                                    .is_some()
555                            }
556                            _ => {
557                                false
558                            }
559                        })
560                        .is_some());
561                }
562            }
563
564            mod with_camera_offset_from_origin {
565                use super::*;
566                use crate::{Component, GameCommandQueue};
567
568                fn make_basic_results() -> Vec<QueryResultList> {
569                    let results = vec![
570                        QueryResultList::new(vec![]),
571                        QueryResultList::new(vec![]),
572                        QueryResultList::new(vec![]),
573                        QueryResultList::new(vec![QueryResult::new(
574                            Entity(1),
575                            StoredComponentList::new(vec![
576                                Rc::new(RefCell::new(Box::new(TerminalCamera {
577                                    field_of_view: Dimensions2d::new(5, 5),
578                                    is_main: true,
579                                }))),
580                                Rc::new(RefCell::new(Box::new(TerminalTransform {
581                                    coords: IntCoords2d::new(-3, 2),
582                                }))),
583                            ]),
584                        )]),
585                    ];
586
587                    results
588                }
589
590                #[test]
591                fn pos_is_correct_for_top_left() {
592                    let mut results = make_basic_results();
593
594                    results[0].push(QueryResult::new(
595                        Entity(0),
596                        StoredComponentList::new(vec![Rc::new(RefCell::new(Box::new(Text {
597                            value: String::from("T"),
598                            anchor: UiAnchor::TopLeft,
599                            justification: Alignment::Left,
600                            offset: IntCoords2d::zero(),
601                            foreground_color: None,
602                            background_color: None,
603                        })))]),
604                    ));
605
606                    let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
607
608                    update_text_ui(results, Rc::clone(&commands));
609
610                    assert!(commands
611                        .borrow()
612                        .queue()
613                        .iter()
614                        .find(|c| match c {
615                            GameCommand::AddEntity(comps) => {
616                                comps
617                                    .iter()
618                                    .find(|comp| {
619                                        comp.component_name() == TerminalTransform::name()
620                                            && (TerminalTransform::cast(&***comp)).unwrap().coords
621                                                == IntCoords2d::new(-3, 2)
622                                    })
623                                    .is_some()
624                            }
625                            _ => {
626                                false
627                            }
628                        })
629                        .is_some());
630                }
631
632                #[test]
633                fn pos_is_correct_for_middle_top() {
634                    let mut results = make_basic_results();
635
636                    results[0].push(QueryResult::new(
637                        Entity(0),
638                        StoredComponentList::new(vec![Rc::new(RefCell::new(Box::new(Text {
639                            value: String::from("T"),
640                            anchor: UiAnchor::MiddleTop,
641                            justification: Alignment::Left,
642                            offset: IntCoords2d::zero(),
643                            foreground_color: None,
644                            background_color: None,
645                        })))]),
646                    ));
647
648                    let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
649
650                    update_text_ui(results, Rc::clone(&commands));
651
652                    assert!(commands
653                        .borrow()
654                        .queue()
655                        .iter()
656                        .find(|c| match c {
657                            GameCommand::AddEntity(comps) => {
658                                comps
659                                    .iter()
660                                    .find(|comp| {
661                                        comp.component_name() == TerminalTransform::name()
662                                            && (TerminalTransform::cast(&***comp)).unwrap().coords
663                                                == IntCoords2d::new(-1, 2)
664                                    })
665                                    .is_some()
666                            }
667                            _ => {
668                                false
669                            }
670                        })
671                        .is_some());
672                }
673
674                #[test]
675                fn pos_is_correct_for_top_right() {
676                    let mut results = make_basic_results();
677
678                    results[0].push(QueryResult::new(
679                        Entity(0),
680                        StoredComponentList::new(vec![Rc::new(RefCell::new(Box::new(Text {
681                            value: String::from("T"),
682                            anchor: UiAnchor::TopRight,
683                            justification: Alignment::Left,
684                            offset: IntCoords2d::zero(),
685                            foreground_color: None,
686                            background_color: None,
687                        })))]),
688                    ));
689
690                    let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
691
692                    update_text_ui(results, Rc::clone(&commands));
693
694                    assert!(commands
695                        .borrow()
696                        .queue()
697                        .iter()
698                        .find(|c| match c {
699                            GameCommand::AddEntity(comps) => {
700                                comps
701                                    .iter()
702                                    .find(|comp| {
703                                        comp.component_name() == TerminalTransform::name()
704                                            && (TerminalTransform::cast(&***comp)).unwrap().coords
705                                                == IntCoords2d::new(1, 2)
706                                    })
707                                    .is_some()
708                            }
709                            _ => {
710                                false
711                            }
712                        })
713                        .is_some());
714                }
715
716                #[test]
717                fn pos_is_correct_for_middle_right() {
718                    let mut results = make_basic_results();
719
720                    results[0].push(QueryResult::new(
721                        Entity(0),
722                        StoredComponentList::new(vec![Rc::new(RefCell::new(Box::new(Text {
723                            value: String::from("T"),
724                            anchor: UiAnchor::MiddleRight,
725                            justification: Alignment::Left,
726                            offset: IntCoords2d::zero(),
727                            foreground_color: None,
728                            background_color: None,
729                        })))]),
730                    ));
731
732                    let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
733
734                    update_text_ui(results, Rc::clone(&commands));
735
736                    assert!(commands
737                        .borrow()
738                        .queue()
739                        .iter()
740                        .find(|c| match c {
741                            GameCommand::AddEntity(comps) => {
742                                comps
743                                    .iter()
744                                    .find(|comp| {
745                                        comp.component_name() == TerminalTransform::name()
746                                            && (TerminalTransform::cast(&***comp)).unwrap().coords
747                                                == IntCoords2d::new(1, 4)
748                                    })
749                                    .is_some()
750                            }
751                            _ => {
752                                false
753                            }
754                        })
755                        .is_some());
756                }
757
758                #[test]
759                fn pos_is_correct_for_bottom_right() {
760                    let mut results = make_basic_results();
761
762                    results[0].push(QueryResult::new(
763                        Entity(0),
764                        StoredComponentList::new(vec![Rc::new(RefCell::new(Box::new(Text {
765                            value: String::from("T"),
766                            anchor: UiAnchor::BottomRight,
767                            justification: Alignment::Left,
768                            offset: IntCoords2d::zero(),
769                            foreground_color: None,
770                            background_color: None,
771                        })))]),
772                    ));
773
774                    let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
775
776                    update_text_ui(results, Rc::clone(&commands));
777
778                    assert!(commands
779                        .borrow()
780                        .queue()
781                        .iter()
782                        .find(|c| match c {
783                            GameCommand::AddEntity(comps) => {
784                                comps
785                                    .iter()
786                                    .find(|comp| {
787                                        comp.component_name() == TerminalTransform::name()
788                                            && (TerminalTransform::cast(&***comp)).unwrap().coords
789                                                == IntCoords2d::new(1, 6)
790                                    })
791                                    .is_some()
792                            }
793                            _ => {
794                                false
795                            }
796                        })
797                        .is_some());
798                }
799
800                #[test]
801                fn pos_is_correct_for_middle_bottom() {
802                    let mut results = make_basic_results();
803
804                    results[0].push(QueryResult::new(
805                        Entity(0),
806                        StoredComponentList::new(vec![Rc::new(RefCell::new(Box::new(Text {
807                            value: String::from("T"),
808                            anchor: UiAnchor::MiddleBottom,
809                            justification: Alignment::Left,
810                            offset: IntCoords2d::zero(),
811                            foreground_color: None,
812                            background_color: None,
813                        })))]),
814                    ));
815
816                    let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
817
818                    update_text_ui(results, Rc::clone(&commands));
819
820                    assert!(commands
821                        .borrow()
822                        .queue()
823                        .iter()
824                        .find(|c| match c {
825                            GameCommand::AddEntity(comps) => {
826                                comps
827                                    .iter()
828                                    .find(|comp| {
829                                        comp.component_name() == TerminalTransform::name()
830                                            && (TerminalTransform::cast(&***comp)).unwrap().coords
831                                                == IntCoords2d::new(-1, 6)
832                                    })
833                                    .is_some()
834                            }
835                            _ => {
836                                false
837                            }
838                        })
839                        .is_some());
840                }
841
842                #[test]
843                fn pos_is_correct_for_bottom_left() {
844                    let mut results = make_basic_results();
845
846                    results[0].push(QueryResult::new(
847                        Entity(0),
848                        StoredComponentList::new(vec![Rc::new(RefCell::new(Box::new(Text {
849                            value: String::from("T"),
850                            anchor: UiAnchor::BottomLeft,
851                            justification: Alignment::Left,
852                            offset: IntCoords2d::zero(),
853                            foreground_color: None,
854                            background_color: None,
855                        })))]),
856                    ));
857
858                    let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
859
860                    update_text_ui(results, Rc::clone(&commands));
861
862                    assert!(commands
863                        .borrow()
864                        .queue()
865                        .iter()
866                        .find(|c| match c {
867                            GameCommand::AddEntity(comps) => {
868                                comps
869                                    .iter()
870                                    .find(|comp| {
871                                        comp.component_name() == TerminalTransform::name()
872                                            && (TerminalTransform::cast(&***comp)).unwrap().coords
873                                                == IntCoords2d::new(-3, 6)
874                                    })
875                                    .is_some()
876                            }
877                            _ => {
878                                false
879                            }
880                        })
881                        .is_some());
882                }
883
884                #[test]
885                fn pos_is_correct_for_middle_left() {
886                    let mut results = make_basic_results();
887
888                    results[0].push(QueryResult::new(
889                        Entity(0),
890                        StoredComponentList::new(vec![Rc::new(RefCell::new(Box::new(Text {
891                            value: String::from("T"),
892                            anchor: UiAnchor::MiddleLeft,
893                            justification: Alignment::Left,
894                            offset: IntCoords2d::zero(),
895                            foreground_color: None,
896                            background_color: None,
897                        })))]),
898                    ));
899
900                    let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
901
902                    update_text_ui(results, Rc::clone(&commands));
903
904                    assert!(commands
905                        .borrow()
906                        .queue()
907                        .iter()
908                        .find(|c| match c {
909                            GameCommand::AddEntity(comps) => {
910                                comps
911                                    .iter()
912                                    .find(|comp| {
913                                        comp.component_name() == TerminalTransform::name()
914                                            && (TerminalTransform::cast(&***comp)).unwrap().coords
915                                                == IntCoords2d::new(-3, 4)
916                                    })
917                                    .is_some()
918                            }
919                            _ => {
920                                false
921                            }
922                        })
923                        .is_some());
924                }
925            }
926        }
927
928        mod world_text {
929            use crate::{Component, GameCommandQueue};
930
931            use super::*;
932
933            #[test]
934            fn text_goes_to_the_correct_position_with_origin_camera() {
935                let results = vec![
936                    QueryResultList::new(vec![]),
937                    QueryResultList::new(vec![QueryResult::new(
938                        Entity(10),
939                        StoredComponentList::new(vec![
940                            Rc::new(RefCell::new(Box::new(WorldText {
941                                value: String::from("T"),
942                                justification: Alignment::Left,
943                                offset: IntCoords2d::zero(),
944                                background_color: None,
945                                foreground_color: None,
946                            }))),
947                            Rc::new(RefCell::new(Box::new(TerminalTransform {
948                                coords: IntCoords2d::new(5, 3),
949                            }))),
950                        ]),
951                    )]),
952                    QueryResultList::new(vec![]),
953                    QueryResultList::new(vec![QueryResult::new(
954                        Entity(1),
955                        StoredComponentList::new(vec![
956                            Rc::new(RefCell::new(Box::new(TerminalCamera {
957                                field_of_view: Dimensions2d::new(10, 10),
958                                is_main: true,
959                            }))),
960                            Rc::new(RefCell::new(Box::new(TerminalTransform {
961                                coords: IntCoords2d::zero(),
962                            }))),
963                        ]),
964                    )]),
965                ];
966
967                let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
968
969                update_text_ui(results, Rc::clone(&commands));
970
971                assert!(commands
972                    .borrow()
973                    .queue()
974                    .iter()
975                    .find(|c| match c {
976                        GameCommand::AddEntity(comps) => {
977                            comps
978                                .iter()
979                                .find(|comp| {
980                                    comp.component_name() == TerminalTransform::name()
981                                        && (TerminalTransform::cast(&***comp)).unwrap().coords
982                                            == IntCoords2d::new(5, 3)
983                                })
984                                .is_some()
985                        }
986                        _ => {
987                            false
988                        }
989                    })
990                    .is_some());
991            }
992
993            #[test]
994            fn text_goes_to_the_correct_position_with_offset_camera() {
995                let results = vec![
996                    QueryResultList::new(vec![]),
997                    QueryResultList::new(vec![QueryResult::new(
998                        Entity(10),
999                        StoredComponentList::new(vec![
1000                            Rc::new(RefCell::new(Box::new(WorldText {
1001                                value: String::from("T"),
1002                                justification: Alignment::Left,
1003                                offset: IntCoords2d::zero(),
1004                                background_color: None,
1005                                foreground_color: None,
1006                            }))),
1007                            Rc::new(RefCell::new(Box::new(TerminalTransform {
1008                                coords: IntCoords2d::new(5, 3),
1009                            }))),
1010                        ]),
1011                    )]),
1012                    QueryResultList::new(vec![]),
1013                    QueryResultList::new(vec![QueryResult::new(
1014                        Entity(1),
1015                        StoredComponentList::new(vec![
1016                            Rc::new(RefCell::new(Box::new(TerminalCamera {
1017                                field_of_view: Dimensions2d::new(10, 10),
1018                                is_main: true,
1019                            }))),
1020                            Rc::new(RefCell::new(Box::new(TerminalTransform {
1021                                coords: IntCoords2d::new(-5, 8),
1022                            }))),
1023                        ]),
1024                    )]),
1025                ];
1026
1027                let commands = Rc::new(RefCell::new(GameCommandQueue::new()));
1028
1029                update_text_ui(results, Rc::clone(&commands));
1030
1031                assert!(commands
1032                    .borrow()
1033                    .queue()
1034                    .iter()
1035                    .find(|c| match c {
1036                        GameCommand::AddEntity(comps) => {
1037                            comps
1038                                .iter()
1039                                .find(|comp| {
1040                                    comp.component_name() == TerminalTransform::name()
1041                                        && (TerminalTransform::cast(&***comp)).unwrap().coords
1042                                            == IntCoords2d::new(5, 3)
1043                                })
1044                                .is_some()
1045                        }
1046                        _ => {
1047                            false
1048                        }
1049                    })
1050                    .is_some());
1051            }
1052        }
1053    }
1054}