tty_form/step/
compound.rs

1use crossterm::event::{KeyCode, KeyEvent};
2use tty_interface::{pos, Interface, Position};
3
4use crate::{
5    control::Control,
6    dependency::{Action, DependencyState},
7    style::{error_style, muted_style},
8    text::{
9        get_segment_length, set_segment_style, set_segment_subset_style, DrawerContents, Segment,
10        Text,
11    },
12    utility::render_segment,
13    Form,
14};
15
16use super::{InputResult, Step};
17
18/// A single-line step which controls multple controls including static and input elements.
19///
20/// # Examples
21/// ```
22/// use tty_form::{
23///     Form,
24///     step::{Step, CompoundStep},
25///     control::{Control, StaticText, TextInput},
26/// };
27///
28/// let mut form = Form::new();
29///
30/// let mut step = CompoundStep::new();
31/// StaticText::new("Welcome, ").add_to(&mut step);
32/// TextInput::new("Enter your name:", false).add_to(&mut step);
33/// step.add_to(&mut form);
34/// ```
35pub struct CompoundStep {
36    index: Option<usize>,
37    controls: Vec<Box<dyn Control>>,
38    max_line_length: Option<u16>,
39    active_control: usize,
40    max_control: usize,
41}
42
43impl CompoundStep {
44    /// Create a new compound step with no controls.
45    pub fn new() -> Self {
46        Self {
47            index: None,
48            controls: Vec::new(),
49            max_line_length: None,
50            active_control: 0,
51            max_control: 0,
52        }
53    }
54
55    /// Append the specified control to this step.
56    pub fn add_control(&mut self, control: Box<dyn Control>) {
57        self.controls.push(control);
58    }
59
60    /// Set this step's maximum total line length.
61    pub fn set_max_line_length(&mut self, max_length: u16) {
62        self.max_line_length = Some(max_length);
63    }
64
65    /// Advance the step's state to the next control. Returns true if we've reached the end of this
66    /// step and the form should advance to the next.
67    fn advance_control(&mut self) -> bool {
68        let mut reached_last_control = false;
69        loop {
70            if self.active_control + 1 >= self.controls.len() {
71                reached_last_control = true;
72                break;
73            }
74
75            self.active_control += 1;
76
77            if self.controls[self.active_control].focusable() {
78                break;
79            }
80        }
81
82        // Advance the max_control past unfocusable controls
83        if self.active_control > self.max_control {
84            self.max_control = self.active_control;
85            loop {
86                if self.max_control + 1 >= self.controls.len() {
87                    break;
88                }
89
90                if !self.controls[self.max_control + 1].focusable() {
91                    self.max_control += 1;
92                } else {
93                    break;
94                }
95            }
96        }
97
98        reached_last_control
99    }
100
101    /// Retreat the step's state to the previous control. Returns true if we've reached the start
102    /// of this step and the form should retreat to the previous.
103    fn retreat_control(&mut self) -> bool {
104        loop {
105            if self.active_control == 0 {
106                return true;
107            }
108
109            self.active_control -= 1;
110
111            if self.controls[self.active_control].focusable() {
112                break;
113            }
114        }
115
116        false
117    }
118}
119
120impl Step for CompoundStep {
121    fn initialize(&mut self, dependency_state: &mut DependencyState, index: usize) {
122        self.index = Some(index);
123
124        // Advance to the first focusable control, since the first might be a static element
125        if !self.controls[0].focusable() {
126            self.advance_control();
127        }
128
129        // Register any evaluations in state for this step
130        for (control_index, control) in self.controls.iter().enumerate() {
131            for (id, evaluation) in control.evaluation() {
132                dependency_state.register_evaluation(&id, index, control_index);
133
134                let value = control.evaluate(&evaluation);
135                dependency_state.update_evaluation(&id, value);
136            }
137        }
138    }
139
140    fn render(
141        &self,
142        interface: &mut Interface,
143        dependency_state: &DependencyState,
144        mut position: Position,
145        is_focused: bool,
146    ) -> u16 {
147        interface.clear_line(position.y());
148
149        let mut cursor_position = None;
150        for (control_index, control) in self.controls.iter().enumerate() {
151            let (mut segment, cursor_offset) = control.text();
152
153            // If this is the focused control, let it drive the overall cursor position
154            if control_index == self.active_control {
155                if let Some(offset) = cursor_offset {
156                    cursor_position = Some(pos!(position.x() + offset, position.y()));
157                }
158            }
159
160            // Resolve this control's dependency and update rendering accordingly
161            let mut should_hide = false;
162            if let Some((id, action)) = control.dependency() {
163                let control_touched = control_index <= self.max_control;
164                let evaluation_result = dependency_state.get_evaluation(&id);
165
166                match action {
167                    Action::Hide => {
168                        if control_touched && evaluation_result {
169                            // Determine if the control's dependency source is focused
170                            let (step_index, control_index) = dependency_state.get_source(&id);
171                            let source_is_focused = step_index == self.index.unwrap()
172                                && control_index == self.active_control;
173
174                            // Either render this control muted or hide it, depending on focus
175                            if source_is_focused {
176                                set_segment_style(&mut segment, muted_style());
177                            } else {
178                                should_hide = true;
179                            }
180                        }
181                    }
182                    Action::Show => should_hide = !evaluation_result,
183                }
184            }
185
186            // If this step is too-long, render the tail as an error
187            if let Some(max_length) = self.max_line_length {
188                let segment_length = get_segment_length(&segment) as u16;
189                if position.x() + segment_length > max_length {
190                    let error_starts_at = max_length - position.x();
191                    set_segment_subset_style(
192                        &mut segment,
193                        error_starts_at.into(),
194                        segment_length.into(),
195                        error_style(),
196                    );
197                }
198            }
199
200            if !should_hide {
201                position = render_segment(interface, position, segment);
202            }
203        }
204
205        if is_focused {
206            interface.set_cursor(cursor_position);
207        }
208
209        1
210    }
211
212    fn update(
213        &mut self,
214        dependency_state: &mut DependencyState,
215        input: KeyEvent,
216    ) -> Option<InputResult> {
217        match input.code {
218            KeyCode::Enter | KeyCode::Tab => {
219                if self.advance_control() {
220                    return Some(InputResult::AdvanceForm);
221                }
222            }
223            KeyCode::Esc | KeyCode::BackTab => {
224                if self.retreat_control() {
225                    return Some(InputResult::RetreatForm);
226                }
227            }
228            _ => {
229                let control = &mut self.controls[self.active_control];
230                control.update(input);
231
232                // If this control has an evaluation, update its dependency state
233                if let Some((id, evaluation)) = control.evaluation() {
234                    let value = control.evaluate(&evaluation);
235                    dependency_state.update_evaluation(&id, value);
236                }
237            }
238        }
239
240        None
241    }
242
243    fn help(&self) -> Segment {
244        self.controls[self.active_control]
245            .help()
246            .unwrap_or(Text::new(String::new()).as_segment())
247    }
248
249    fn drawer(&self) -> Option<DrawerContents> {
250        self.controls[self.active_control].drawer()
251    }
252
253    fn result(&self, dependency_state: &DependencyState) -> String {
254        let mut result = String::new();
255
256        for control in &self.controls {
257            if let Some((id, action)) = control.dependency() {
258                let evaluation_result = dependency_state.get_evaluation(&id);
259                match action {
260                    Action::Hide => {
261                        if evaluation_result {
262                            continue;
263                        }
264                    }
265                    Action::Show => {
266                        if !evaluation_result {
267                            continue;
268                        }
269                    }
270                }
271            }
272
273            let (segments, _) = control.text();
274            segments
275                .iter()
276                .for_each(|text| result.push_str(text.content()));
277        }
278
279        result.push('\n');
280
281        result
282    }
283
284    fn add_to(self, form: &mut Form) {
285        form.add_step(Box::new(self));
286    }
287}