klondike_lib/display/
help.rs

1use itertools::Itertools;
2use std::{convert::TryFrom, fmt};
3use termion::color;
4
5use crate::utils::{format_str::FormattedString, str::CharacterLength};
6
7use super::{
8    frame::{self, FrameWidget, Title},
9    geometry, Widget,
10};
11
12lazy_static! {
13    static ref MARGIN: geometry::SideOffsets2D<u16> = geometry::SideOffsets2D::new(1, 2, 1, 2);
14    static ref BORDER: geometry::SideOffsets2D<u16> = geometry::SideOffsets2D::new_all_same(1);
15    static ref PADDING: geometry::SideOffsets2D<u16> = geometry::SideOffsets2D::new(1, 2, 1, 2);
16}
17
18#[derive(Debug)]
19pub struct HelpWidget {
20    pub bounds: geometry::Rect<u16>,
21}
22
23impl Widget for HelpWidget {
24    fn bounds(&self) -> geometry::Rect<u16> {
25        self.bounds
26    }
27}
28
29impl fmt::Display for HelpWidget {
30    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
31        let frame_bounds = self.bounds.inner_rect(*MARGIN);
32        let inner_bounds = frame_bounds.inner_rect(*BORDER + *PADDING);
33
34        let frame_display = FrameWidget {
35            bounds: frame_bounds,
36            top_title: Some(Title::center("H E L P")),
37            bottom_title: Some(Title::right("Press any key to continue . . .")),
38            frame_style: &frame::DOUBLE,
39        };
40
41        write!(fmt, "{}", frame_display)?;
42
43        let inner_top_left = inner_bounds.origin;
44        let inner_top_middle = inner_top_left + geometry::vec2(inner_bounds.size.width / 2 + 1, 0);
45
46        for item in left_column_items(inner_top_left) {
47            write!(fmt, "{}", item)?;
48        }
49
50        for item in right_column_items(inner_top_middle) {
51            write!(fmt, "{}", item)?;
52        }
53
54        Ok(())
55    }
56}
57
58fn left_column_items(origin: geometry::Point2D<u16>) -> Vec<HelpItemWidget> {
59    let mut coord_iter = (0..).map(|index| origin + geometry::vec2(0, index));
60
61    vec![
62        HelpItemWidget::Mapping {
63            origin: coord_iter.next().unwrap(),
64            keys: HelpItemKeys::List(vec!["h", "j", "k", "l"]),
65            description: "Move",
66        },
67        HelpItemWidget::Mapping {
68            origin: coord_iter.next().unwrap(),
69            keys: HelpItemKeys::List(vec!["←", "↓", "↑", "→"]),
70            description: "Move",
71        },
72        HelpItemWidget::Skip {
73            origin: coord_iter.next().unwrap(),
74        },
75        HelpItemWidget::Mapping {
76            origin: coord_iter.next().unwrap(),
77            keys: HelpItemKeys::Single("s"),
78            description: "Go to Stock/Deck",
79        },
80        HelpItemWidget::Mapping {
81            origin: coord_iter.next().unwrap(),
82            keys: HelpItemKeys::Single("t"),
83            description: "Go to Talon/Waste",
84        },
85        HelpItemWidget::Mapping {
86            origin: coord_iter.next().unwrap(),
87            keys: HelpItemKeys::Single("f"),
88            description: "Go to next Foundation",
89        },
90        HelpItemWidget::Mapping {
91            origin: coord_iter.next().unwrap(),
92            keys: HelpItemKeys::Single("-"),
93            description: "Go back to previous area",
94        },
95    ]
96}
97
98fn right_column_items(origin: geometry::Point2D<u16>) -> Vec<HelpItemWidget> {
99    let mut coord_iter = (0..).map(|index| origin + geometry::vec2(0, index));
100
101    vec![
102        HelpItemWidget::Mapping {
103            origin: coord_iter.next().unwrap(),
104            keys: HelpItemKeys::Range("F1", "F4"),
105            description: "Go to Foundation",
106        },
107        HelpItemWidget::Mapping {
108            origin: coord_iter.next().unwrap(),
109            keys: HelpItemKeys::Range("1", "7"),
110            description: "Go to Tableaux",
111        },
112        HelpItemWidget::Skip {
113            origin: coord_iter.next().unwrap(),
114        },
115        HelpItemWidget::Mapping {
116            origin: coord_iter.next().unwrap(),
117            keys: HelpItemKeys::List(vec!["SPACE", "RETURN"]),
118            description: "Pick Up/Activate",
119        },
120        HelpItemWidget::Mapping {
121            origin: coord_iter.next().unwrap(),
122            keys: HelpItemKeys::Single("ESC"),
123            description: "Return Held Cards",
124        },
125        HelpItemWidget::Skip {
126            origin: coord_iter.next().unwrap(),
127        },
128        HelpItemWidget::Mapping {
129            origin: coord_iter.next().unwrap(),
130            keys: HelpItemKeys::Single("?"),
131            description: "Help",
132        },
133        HelpItemWidget::Mapping {
134            origin: coord_iter.next().unwrap(),
135            keys: HelpItemKeys::Single("q"),
136            description: "Quit",
137        },
138    ]
139}
140
141#[derive(Debug)]
142enum HelpItemKeys {
143    Single(&'static str),
144    List(Vec<&'static str>),
145    Range(&'static str, &'static str),
146}
147
148impl HelpItemKeys {
149    fn len(&self) -> usize {
150        match self {
151            Self::Single(key) => key.char_len(),
152            Self::List(keys) => keys.iter().map(|key| key.char_len()).intersperse(3).sum(),
153            Self::Range(start_key, end_key) => start_key.char_len() + end_key.char_len() + 5,
154        }
155    }
156}
157
158impl fmt::Display for HelpItemKeys {
159    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
160        match self {
161            Self::Single(key) => {
162                write!(fmt, "{key_style}{key}", key_style = key_style(), key = key)?;
163            }
164            Self::List(keys) => {
165                let formatted_keys_iter = keys
166                    .iter()
167                    .map(|key| FormattedString::new_with_formatting(key_style()).push_content(key))
168                    .intersperse(
169                        FormattedString::new_with_formatting(reset_style()).push_content(" / "),
170                    );
171
172                for formatted_key in formatted_keys_iter {
173                    write!(fmt, "{}", formatted_key)?;
174                }
175            }
176            Self::Range(start_key, end_key) => {
177                write!(
178                    fmt,
179                    "{key_style}{start_key}{reset} ... {key_style}{end_key}",
180                    reset = reset_style(),
181                    key_style = key_style(),
182                    start_key = start_key,
183                    end_key = end_key,
184                )?;
185            }
186        };
187
188        Ok(())
189    }
190}
191
192#[derive(Debug)]
193enum HelpItemWidget {
194    Mapping {
195        origin: geometry::Point2D<u16>,
196        keys: HelpItemKeys,
197        description: &'static str,
198    },
199    Skip {
200        origin: geometry::Point2D<u16>,
201    },
202}
203
204impl Widget for HelpItemWidget {
205    fn bounds(&self) -> geometry::Rect<u16> {
206        match self {
207            Self::Mapping {
208                origin,
209                keys,
210                description,
211            } => {
212                let length = u16::try_from(keys.len() + description.char_len() + 4).unwrap();
213                geometry::Rect::new(*origin, geometry::size2(length, 0))
214            }
215            Self::Skip { origin } => geometry::Rect::new(*origin, geometry::Size2D::zero()),
216        }
217    }
218}
219
220impl fmt::Display for HelpItemWidget {
221    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
222        if let Self::Mapping {
223            origin,
224            keys,
225            description,
226        } = self
227        {
228            let goto = geometry::goto(*origin);
229
230            write!(
231                fmt,
232                "{goto}{keys}{reset} :  {desc_style}{desc}",
233                goto = goto,
234                reset = reset_style(),
235                desc_style = description_style(),
236                keys = keys,
237                desc = description,
238            )?;
239        }
240
241        Ok(())
242    }
243}
244
245fn key_style() -> impl fmt::Display {
246    color::Fg(color::Cyan)
247}
248
249fn reset_style() -> impl fmt::Display {
250    color::Fg(color::Reset)
251}
252
253fn description_style() -> impl fmt::Display {
254    color::Fg(color::White)
255}