1use crate::error::FigletError;
25
26const MAX_FILTER_NAME_BYTES: usize = 64;
32
33const FILTER_NAMES: &[&str] = &[
39 "crop",
40 "gay",
41 "metal",
42 "flip",
43 "flop",
44 "rotate180",
45 "rotateleft",
46 "rotateright",
47 "border",
48 "nothing",
49];
50
51#[non_exhaustive]
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub enum Color {
69 Named(NamedColor),
71 Index(u8),
73 Rgb(u8, u8, u8),
75}
76
77impl Default for Color {
78 fn default() -> Self {
79 Self::Named(NamedColor::White)
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
89#[allow(missing_docs)]
90pub enum NamedColor {
91 Black,
92 Red,
93 Green,
94 Yellow,
95 Blue,
96 Magenta,
97 Cyan,
98 White,
99 BrightBlack,
100 BrightRed,
101 BrightGreen,
102 BrightYellow,
103 BrightBlue,
104 BrightMagenta,
105 BrightCyan,
106 BrightWhite,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119pub struct Cell {
120 pub ch: char,
122 pub fg: Color,
124 pub bg: Option<Color>,
126 pub attrs: u8,
128}
129
130impl Cell {
131 #[must_use]
134 pub fn new(ch: char) -> Self {
135 Self {
136 ch,
137 fg: Color::default(),
138 bg: None,
139 attrs: 0,
140 }
141 }
142
143 #[must_use]
145 pub fn blank() -> Self {
146 Self::new(' ')
147 }
148
149 #[must_use]
154 pub fn is_blank(&self) -> bool {
155 self.ch == ' '
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct RenderGrid {
173 pub cells: Vec<Vec<Cell>>,
176 pub width: u32,
178 pub height: u32,
180}
181
182impl RenderGrid {
183 #[must_use]
185 pub fn empty() -> Self {
186 Self {
187 cells: Vec::new(),
188 width: 0,
189 height: 0,
190 }
191 }
192
193 #[must_use]
196 pub fn blank(width: u32, height: u32) -> Self {
197 let w = width as usize;
198 let h = height as usize;
199 let cells = (0..h).map(|_| vec![Cell::blank(); w]).collect();
200 Self {
201 cells,
202 width,
203 height,
204 }
205 }
206
207 #[must_use]
210 pub fn from_rows(mut rows: Vec<Vec<Cell>>) -> Self {
211 let width = rows.iter().map(Vec::len).max().unwrap_or(0);
212 for row in rows.iter_mut() {
213 if row.len() < width {
214 row.resize(width, Cell::blank());
215 }
216 }
217 let height = rows.len();
218 Self {
219 cells: rows,
220 width: width as u32,
221 height: height as u32,
222 }
223 }
224
225 #[must_use]
229 pub fn from_text_rows(rows: &[String]) -> Self {
230 let cells: Vec<Vec<Cell>> = rows
231 .iter()
232 .map(|line| line.chars().map(Cell::new).collect())
233 .collect();
234 Self::from_rows(cells)
235 }
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
246pub enum Filter {
247 Crop,
249 Gay,
251 Metal,
253 Flip,
255 Flop,
257 Rotate180,
259 RotateLeft,
261 RotateRight,
263 Border,
265 Nothing,
267}
268
269impl Filter {
270 #[must_use]
272 pub const fn name(&self) -> &'static str {
273 match self {
274 Filter::Crop => "crop",
275 Filter::Gay => "gay",
276 Filter::Metal => "metal",
277 Filter::Flip => "flip",
278 Filter::Flop => "flop",
279 Filter::Rotate180 => "rotate180",
280 Filter::RotateLeft => "rotateleft",
281 Filter::RotateRight => "rotateright",
282 Filter::Border => "border",
283 Filter::Nothing => "nothing",
284 }
285 }
286
287 fn from_name(name: &str) -> Option<Filter> {
294 Some(match name {
295 "crop" => Filter::Crop,
296 "gay" => Filter::Gay,
297 "metal" => Filter::Metal,
298 "flip" => Filter::Flip,
299 "flop" => Filter::Flop,
300 "rotate180" => Filter::Rotate180,
301 "rotateleft" => Filter::RotateLeft,
302 "rotateright" => Filter::RotateRight,
303 "border" => Filter::Border,
304 "nothing" => Filter::Nothing,
305 _ => return None,
306 })
307 }
308}
309
310#[derive(Debug, Clone, Default, PartialEq, Eq)]
349pub struct FilterChain {
350 filters: Vec<Filter>,
351}
352
353impl FilterChain {
354 #[must_use]
358 pub fn new() -> Self {
359 Self::default()
360 }
361
362 #[must_use]
365 pub fn push(mut self, filter: Filter) -> Self {
366 self.filters.push(filter);
367 self
368 }
369
370 pub fn parse(spec: &str) -> Result<FilterChain, FigletError> {
383 let mut filters = Vec::new();
384 if spec.is_empty() {
385 return Ok(Self { filters });
386 }
387 for segment in spec.split(':') {
388 if segment.is_empty() || segment.len() > MAX_FILTER_NAME_BYTES {
389 return Err(FigletError::UnknownFilter {
390 name: segment.to_owned(),
391 available: FILTER_NAMES.iter().map(|&s| s.to_owned()).collect(),
392 });
393 }
394 match Filter::from_name(segment) {
395 Some(f) => filters.push(f),
396 None => {
397 return Err(FigletError::UnknownFilter {
398 name: segment.to_owned(),
399 available: FILTER_NAMES.iter().map(|&s| s.to_owned()).collect(),
400 });
401 }
402 }
403 }
404 Ok(Self { filters })
405 }
406
407 #[must_use]
409 pub fn len(&self) -> usize {
410 self.filters.len()
411 }
412
413 #[must_use]
416 pub fn is_empty(&self) -> bool {
417 self.filters.is_empty()
418 }
419
420 #[must_use]
422 pub fn filters(&self) -> &[Filter] {
423 &self.filters
424 }
425
426 pub fn apply(&self, grid: RenderGrid) -> Result<RenderGrid, FigletError> {
444 let mut current = grid;
445 for filter in &self.filters {
446 current = dispatch(*filter, current)?;
447 }
448 Ok(current)
449 }
450}
451
452fn dispatch(filter: Filter, grid: RenderGrid) -> Result<RenderGrid, FigletError> {
456 match filter {
457 Filter::Nothing => Ok(apply_nothing(grid)),
458 #[cfg(feature = "filter-crop")]
459 Filter::Crop => Ok(apply_crop(grid)),
460 #[cfg(not(feature = "filter-crop"))]
461 Filter::Crop => Err(filter_disabled("crop")),
462 #[cfg(feature = "filter-gay")]
463 Filter::Gay => Ok(apply_gay(grid)),
464 #[cfg(not(feature = "filter-gay"))]
465 Filter::Gay => Err(filter_disabled("gay")),
466 #[cfg(feature = "filter-metal")]
467 Filter::Metal => Ok(apply_metal(grid)),
468 #[cfg(not(feature = "filter-metal"))]
469 Filter::Metal => Err(filter_disabled("metal")),
470 #[cfg(feature = "filter-flip")]
471 Filter::Flip => Ok(apply_flip(grid)),
472 #[cfg(not(feature = "filter-flip"))]
473 Filter::Flip => Err(filter_disabled("flip")),
474 #[cfg(feature = "filter-flop")]
475 Filter::Flop => Ok(apply_flop(grid)),
476 #[cfg(not(feature = "filter-flop"))]
477 Filter::Flop => Err(filter_disabled("flop")),
478 #[cfg(feature = "filter-rotate")]
479 Filter::Rotate180 => Ok(apply_rotate180(grid)),
480 #[cfg(not(feature = "filter-rotate"))]
481 Filter::Rotate180 => Err(filter_disabled("rotate180")),
482 #[cfg(feature = "filter-rotate")]
483 Filter::RotateLeft => Ok(apply_rotate_left(grid)),
484 #[cfg(not(feature = "filter-rotate"))]
485 Filter::RotateLeft => Err(filter_disabled("rotateleft")),
486 #[cfg(feature = "filter-rotate")]
487 Filter::RotateRight => Ok(apply_rotate_right(grid)),
488 #[cfg(not(feature = "filter-rotate"))]
489 Filter::RotateRight => Err(filter_disabled("rotateright")),
490 #[cfg(feature = "filter-border")]
491 Filter::Border => Ok(apply_border(grid)),
492 #[cfg(not(feature = "filter-border"))]
493 Filter::Border => Err(filter_disabled("border")),
494 }
495}
496
497#[allow(dead_code)]
498fn filter_disabled(name: &str) -> FigletError {
499 FigletError::UnknownFilter {
500 name: name.to_owned(),
501 available: FILTER_NAMES.iter().map(|&s| s.to_owned()).collect(),
502 }
503}
504
505fn apply_nothing(grid: RenderGrid) -> RenderGrid {
513 grid
514}
515
516#[cfg(feature = "filter-crop")]
522fn apply_crop(grid: RenderGrid) -> RenderGrid {
523 let h = grid.cells.len();
524 if h == 0 || grid.cells[0].is_empty() {
525 return RenderGrid::empty();
526 }
527 let w = grid.cells[0].len();
528
529 let mut top = h;
533 let mut bottom = 0usize;
534 let mut left = w;
535 let mut right = 0usize;
536
537 for (y, row) in grid.cells.iter().enumerate() {
538 for (x, cell) in row.iter().enumerate() {
539 if !cell.is_blank() {
540 if y < top {
541 top = y;
542 }
543 if y > bottom {
544 bottom = y;
545 }
546 if x < left {
547 left = x;
548 }
549 if x > right {
550 right = x;
551 }
552 }
553 }
554 }
555
556 if top == h {
557 return RenderGrid::empty();
558 }
559
560 let new_h = bottom - top + 1;
561 let new_w = right - left + 1;
562 let mut cells: Vec<Vec<Cell>> = Vec::with_capacity(new_h);
563 for row in grid.cells.iter().skip(top).take(new_h) {
564 cells.push(row[left..=right].to_vec());
565 }
566 RenderGrid {
567 cells,
568 width: new_w as u32,
569 height: new_h as u32,
570 }
571}
572
573#[cfg(feature = "filter-gay")]
582fn apply_gay(grid: RenderGrid) -> RenderGrid {
583 let w = grid.width.max(1);
584 let mut cells = grid.cells;
585 for row in cells.iter_mut() {
586 for (x, cell) in row.iter_mut().enumerate() {
587 let hue = 360.0_f32 * (x as f32 / w as f32);
588 let (r, g, b) = hsv_to_rgb(hue, 1.0, 1.0);
589 cell.fg = Color::Rgb(r, g, b);
590 }
591 }
592 RenderGrid {
593 cells,
594 width: grid.width,
595 height: grid.height,
596 }
597}
598
599#[cfg(feature = "filter-metal")]
607fn apply_metal(grid: RenderGrid) -> RenderGrid {
608 const PALETTE: [NamedColor; 4] = [
609 NamedColor::Cyan,
610 NamedColor::Blue,
611 NamedColor::BrightCyan,
612 NamedColor::BrightBlue,
613 ];
614 let mut cells = grid.cells;
615 for (y, row) in cells.iter_mut().enumerate() {
616 let c = PALETTE[y % PALETTE.len()];
617 for cell in row.iter_mut() {
618 cell.fg = Color::Named(c);
619 }
620 }
621 RenderGrid {
622 cells,
623 width: grid.width,
624 height: grid.height,
625 }
626}
627
628#[cfg(feature = "filter-flip")]
630fn apply_flip(grid: RenderGrid) -> RenderGrid {
631 let mut cells = grid.cells;
632 for row in cells.iter_mut() {
633 row.reverse();
634 }
635 RenderGrid {
636 cells,
637 width: grid.width,
638 height: grid.height,
639 }
640}
641
642#[cfg(feature = "filter-flop")]
644fn apply_flop(grid: RenderGrid) -> RenderGrid {
645 let mut cells = grid.cells;
646 cells.reverse();
647 RenderGrid {
648 cells,
649 width: grid.width,
650 height: grid.height,
651 }
652}
653
654#[cfg(feature = "filter-rotate")]
656fn apply_rotate180(grid: RenderGrid) -> RenderGrid {
657 let mut cells = grid.cells;
658 cells.reverse();
659 for row in cells.iter_mut() {
660 row.reverse();
661 }
662 RenderGrid {
663 cells,
664 width: grid.width,
665 height: grid.height,
666 }
667}
668
669#[cfg(feature = "filter-rotate")]
673fn apply_rotate_left(grid: RenderGrid) -> RenderGrid {
674 let w = grid.width as usize;
675 let h = grid.height as usize;
676 if w == 0 || h == 0 {
677 return RenderGrid::empty();
678 }
679 let mut new_cells: Vec<Vec<Cell>> = (0..w).map(|_| Vec::with_capacity(h)).collect();
680 for x in (0..w).rev() {
685 let row: Vec<Cell> = (0..h).map(|y| grid.cells[y][x]).collect();
686 new_cells[w - 1 - x] = row;
687 }
688 RenderGrid {
689 cells: new_cells,
690 width: h as u32,
691 height: w as u32,
692 }
693}
694
695#[cfg(feature = "filter-rotate")]
699fn apply_rotate_right(grid: RenderGrid) -> RenderGrid {
700 let w = grid.width as usize;
701 let h = grid.height as usize;
702 if w == 0 || h == 0 {
703 return RenderGrid::empty();
704 }
705 let mut new_cells: Vec<Vec<Cell>> = (0..w).map(|_| Vec::with_capacity(h)).collect();
706 for (x, row_out) in new_cells.iter_mut().enumerate().take(w) {
708 *row_out = (0..h).rev().map(|y| grid.cells[y][x]).collect();
709 }
710 RenderGrid {
711 cells: new_cells,
712 width: h as u32,
713 height: w as u32,
714 }
715}
716
717#[cfg(feature = "filter-border")]
723fn apply_border(grid: RenderGrid) -> RenderGrid {
724 let w = grid.width as usize;
725 let h = grid.height as usize;
726 let new_w = w + 2;
727 let new_h = h + 2;
728 let mut cells: Vec<Vec<Cell>> = Vec::with_capacity(new_h);
729
730 let mut top = Vec::with_capacity(new_w);
732 top.push(Cell::new('┌'));
733 for _ in 0..w {
734 top.push(Cell::new('─'));
735 }
736 top.push(Cell::new('┐'));
737 cells.push(top);
738
739 for row in grid.cells {
741 let mut new_row = Vec::with_capacity(new_w);
742 new_row.push(Cell::new('│'));
743 new_row.extend(row);
744 new_row.push(Cell::new('│'));
745 cells.push(new_row);
746 }
747
748 let mut bottom = Vec::with_capacity(new_w);
750 bottom.push(Cell::new('└'));
751 for _ in 0..w {
752 bottom.push(Cell::new('─'));
753 }
754 bottom.push(Cell::new('┘'));
755 cells.push(bottom);
756
757 RenderGrid {
758 cells,
759 width: new_w as u32,
760 height: new_h as u32,
761 }
762}
763
764#[cfg(feature = "filter-gay")]
769fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) {
770 let c = v * s;
771 let h_p = (h % 360.0) / 60.0;
772 let x = c * (1.0 - (h_p % 2.0 - 1.0).abs());
773 let m = v - c;
774 let (r1, g1, b1) = match h_p as u32 {
775 0 => (c, x, 0.0),
776 1 => (x, c, 0.0),
777 2 => (0.0, c, x),
778 3 => (0.0, x, c),
779 4 => (x, 0.0, c),
780 _ => (c, 0.0, x),
781 };
782 let to_u8 = |f: f32| ((f + m) * 255.0).round().clamp(0.0, 255.0) as u8;
783 (to_u8(r1), to_u8(g1), to_u8(b1))
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789
790 #[test]
795 fn cell_footprint_is_bounded() {
796 assert!(
797 std::mem::size_of::<Cell>() <= 24,
798 "Cell size {} exceeds AD-011 budget",
799 std::mem::size_of::<Cell>()
800 );
801 }
802
803 #[test]
804 fn parse_empty_chain_is_ok() {
805 let c = FilterChain::parse("").unwrap();
806 assert!(c.is_empty());
807 }
808
809 #[test]
810 fn parse_single_filter() {
811 let c = FilterChain::parse("crop").unwrap();
812 assert_eq!(c.filters(), &[Filter::Crop]);
813 }
814
815 #[test]
816 fn parse_multi_filter_chain() {
817 let c = FilterChain::parse("crop:flip:border").unwrap();
818 assert_eq!(c.filters(), &[Filter::Crop, Filter::Flip, Filter::Border]);
819 }
820
821 #[test]
822 fn parse_empty_segment_is_unknown_filter() {
823 let err = FilterChain::parse("crop::flip").unwrap_err();
824 match err {
825 FigletError::UnknownFilter { name, available } => {
826 assert_eq!(name, "");
827 assert_eq!(available.len(), 10);
828 }
829 other => panic!("expected UnknownFilter, got {other:?}"),
830 }
831 }
832
833 #[test]
834 fn parse_unknown_name_lists_available() {
835 let err = FilterChain::parse("nosuchfilter").unwrap_err();
836 match err {
837 FigletError::UnknownFilter { name, available } => {
838 assert_eq!(name, "nosuchfilter");
839 assert!(available.contains(&"crop".to_string()));
840 assert!(available.contains(&"nothing".to_string()));
841 }
842 other => panic!("expected UnknownFilter, got {other:?}"),
843 }
844 }
845
846 #[test]
847 fn parse_oversize_name_rejected() {
848 let big = "a".repeat(MAX_FILTER_NAME_BYTES + 1);
849 let err = FilterChain::parse(&big).unwrap_err();
850 assert!(matches!(err, FigletError::UnknownFilter { .. }));
851 }
852
853 #[test]
854 fn programmatic_push_matches_parse() {
855 let manual = FilterChain::new().push(Filter::Crop).push(Filter::Flip);
856 let parsed = FilterChain::parse("crop:flip").unwrap();
857 assert_eq!(manual, parsed);
858 }
859
860 #[test]
861 fn empty_chain_apply_is_identity() {
862 let g = RenderGrid::blank(3, 2);
863 let chain = FilterChain::new();
864 let out = chain.apply(g.clone()).unwrap();
865 assert_eq!(out, g);
866 }
867
868 #[test]
869 fn nothing_filter_is_identity() {
870 let g = RenderGrid::blank(3, 2);
871 let chain = FilterChain::new().push(Filter::Nothing);
872 let out = chain.apply(g.clone()).unwrap();
873 assert_eq!(out, g);
874 }
875
876 #[cfg(feature = "filter-crop")]
877 #[test]
878 fn crop_trims_blank_border() {
879 let mut rows = vec![vec![Cell::blank(); 4]; 4];
880 rows[1][1] = Cell::new('X');
881 rows[1][2] = Cell::new('Y');
882 rows[2][1] = Cell::new('Z');
883 let grid = RenderGrid::from_rows(rows);
884 let chain = FilterChain::new().push(Filter::Crop);
885 let out = chain.apply(grid).unwrap();
886 assert_eq!(out.width, 2);
887 assert_eq!(out.height, 2);
888 assert_eq!(out.cells[0][0].ch, 'X');
889 assert_eq!(out.cells[1][0].ch, 'Z');
890 }
891
892 #[cfg(feature = "filter-crop")]
893 #[test]
894 fn crop_all_blank_returns_empty() {
895 let grid = RenderGrid::blank(4, 4);
896 let out = FilterChain::new().push(Filter::Crop).apply(grid).unwrap();
897 assert_eq!(out.width, 0);
898 assert_eq!(out.height, 0);
899 }
900
901 #[cfg(feature = "filter-flip")]
902 #[test]
903 fn flip_reverses_each_row() {
904 let grid = RenderGrid::from_text_rows(&[String::from("ABCD"), String::from("1234")]);
905 let out = FilterChain::new().push(Filter::Flip).apply(grid).unwrap();
906 assert_eq!(out.cells[0][0].ch, 'D');
907 assert_eq!(out.cells[0][3].ch, 'A');
908 assert_eq!(out.cells[1][0].ch, '4');
909 }
910
911 #[cfg(feature = "filter-flop")]
912 #[test]
913 fn flop_reverses_row_order() {
914 let grid = RenderGrid::from_text_rows(&[String::from("AAA"), String::from("BBB")]);
915 let out = FilterChain::new().push(Filter::Flop).apply(grid).unwrap();
916 assert_eq!(out.cells[0][0].ch, 'B');
917 assert_eq!(out.cells[1][0].ch, 'A');
918 }
919
920 #[cfg(feature = "filter-rotate")]
921 #[test]
922 fn rotate180_inverts() {
923 let grid = RenderGrid::from_text_rows(&[String::from("AB"), String::from("CD")]);
924 let out = FilterChain::new()
925 .push(Filter::Rotate180)
926 .apply(grid)
927 .unwrap();
928 assert_eq!(out.cells[0][0].ch, 'D');
929 assert_eq!(out.cells[0][1].ch, 'C');
930 assert_eq!(out.cells[1][0].ch, 'B');
931 assert_eq!(out.cells[1][1].ch, 'A');
932 }
933
934 #[cfg(feature = "filter-rotate")]
935 #[test]
936 fn rotate_left_swaps_dimensions() {
937 let grid = RenderGrid::from_text_rows(&[String::from("ABC"), String::from("DEF")]);
938 let out = FilterChain::new()
939 .push(Filter::RotateLeft)
940 .apply(grid)
941 .unwrap();
942 assert_eq!(out.width, 2);
945 assert_eq!(out.height, 3);
946 assert_eq!(out.cells[0][0].ch, 'C');
947 assert_eq!(out.cells[0][1].ch, 'F');
948 assert_eq!(out.cells[2][0].ch, 'A');
949 assert_eq!(out.cells[2][1].ch, 'D');
950 }
951
952 #[cfg(feature = "filter-rotate")]
953 #[test]
954 fn rotate_right_swaps_dimensions() {
955 let grid = RenderGrid::from_text_rows(&[String::from("ABC"), String::from("DEF")]);
956 let out = FilterChain::new()
957 .push(Filter::RotateRight)
958 .apply(grid)
959 .unwrap();
960 assert_eq!(out.width, 2);
963 assert_eq!(out.height, 3);
964 assert_eq!(out.cells[0][0].ch, 'D');
965 assert_eq!(out.cells[0][1].ch, 'A');
966 assert_eq!(out.cells[2][0].ch, 'F');
967 assert_eq!(out.cells[2][1].ch, 'C');
968 }
969
970 #[cfg(feature = "filter-border")]
971 #[test]
972 fn border_adds_one_cell_of_padding() {
973 let grid = RenderGrid::from_text_rows(&[String::from("XX")]);
974 let out = FilterChain::new().push(Filter::Border).apply(grid).unwrap();
975 assert_eq!(out.width, 4);
976 assert_eq!(out.height, 3);
977 assert_eq!(out.cells[0][0].ch, '┌');
978 assert_eq!(out.cells[0][3].ch, '┐');
979 assert_eq!(out.cells[2][0].ch, '└');
980 assert_eq!(out.cells[2][3].ch, '┘');
981 assert_eq!(out.cells[1][1].ch, 'X');
982 }
983
984 #[cfg(feature = "filter-gay")]
985 #[test]
986 fn gay_assigns_rgb_per_column() {
987 let grid = RenderGrid::from_text_rows(&[String::from("ABCD")]);
988 let out = FilterChain::new().push(Filter::Gay).apply(grid).unwrap();
989 let c0 = out.cells[0][0].fg;
992 let c1 = out.cells[0][1].fg;
993 assert!(matches!(c0, Color::Rgb(..)));
994 assert!(matches!(c1, Color::Rgb(..)));
995 assert_ne!(c0, c1);
996 }
997
998 #[cfg(feature = "filter-metal")]
999 #[test]
1000 fn metal_cycles_palette_per_row() {
1001 let grid = RenderGrid::from_text_rows(&[
1002 String::from("A"),
1003 String::from("B"),
1004 String::from("C"),
1005 String::from("D"),
1006 String::from("E"),
1007 ]);
1008 let out = FilterChain::new().push(Filter::Metal).apply(grid).unwrap();
1009 assert_eq!(out.cells[0][0].fg, out.cells[4][0].fg);
1011 assert_ne!(out.cells[0][0].fg, out.cells[1][0].fg);
1013 }
1014
1015 #[cfg(all(feature = "filter-flip", feature = "filter-gay"))]
1016 #[test]
1017 fn chain_order_observable_gay_then_flip() {
1018 let grid = RenderGrid::from_text_rows(&[String::from("ABCD")]);
1022 let a = FilterChain::new()
1023 .push(Filter::Gay)
1024 .push(Filter::Flip)
1025 .apply(grid.clone())
1026 .unwrap();
1027 let b = FilterChain::new()
1028 .push(Filter::Flip)
1029 .push(Filter::Gay)
1030 .apply(grid)
1031 .unwrap();
1032 assert_ne!(a.cells[0][0].fg, b.cells[0][0].fg);
1033 }
1034}