Skip to main content

ez_tui/components/concrete/forms/inputs/
text_input.rs

1use crate::components::concrete::forms::inputs::core::{FormFieldValue, InputCpt};
2use crate::{
3    AttrValue, Attribute, Component, EzArgs, EzCptIds, EzEvent, EzMsg, EzState, FormField,
4    FormFieldType, MockComponent, MockProps, Props, State, Theme,
5};
6use eztui_derive::MockProps;
7use ratatui::buffer::Buffer;
8use ratatui::layout::{Alignment, Rect};
9use ratatui::prelude::Widget;
10use ratatui::widgets::{BorderType, Paragraph};
11use std::fmt::Debug;
12use tui_input::Input;
13use tui_input::backend::crossterm::EventHandler;
14/// A component drawing a [`FormField`] of type [`FormFieldType::Text`] or [`FormFieldType::Password`].
15#[derive(Debug, MockProps)]
16pub struct TextInputCpt<FID>
17where
18    FID: EzCptIds,
19{
20    secret: bool,
21    props: Props,
22    #[allow(dead_code)]
23    // I need a phantom if i remove this. So it's better keeping it as i need it in the constructor
24    field: FormField<FID>,
25    input: Input,
26}
27
28impl<FID> TextInputCpt<FID>
29where
30    FID: EzCptIds,
31{
32    /// Create a new [`TextInputCpt`] component with a given form.
33    ///
34    /// # Panics
35    /// If the given field is not of the correct type
36    #[must_use]
37    pub fn clear(field: FormField<FID>) -> Self {
38        assert_eq!(field.field_type(), &FormFieldType::Text {});
39        Self::init(field, false)
40    }
41    /// Create a new [`TextInputCpt`] component with a given form.
42    ///
43    /// # Panics
44    /// If the given field is not of the correct type
45    #[must_use]
46    pub fn secret(field: FormField<FID>) -> Self {
47        assert!(matches!(field.field_type(), &FormFieldType::Password {}));
48        Self::init(field, true)
49    }
50
51    fn init(field: FormField<FID>, secret: bool) -> TextInputCpt<FID> {
52        let mut props = Props::default();
53        props.set(
54            Attribute::Title,
55            AttrValue::Title((field.name(), Alignment::Left)),
56        );
57        Self {
58            secret,
59            props,
60            field,
61            input: Input::default(),
62        }
63    }
64}
65
66impl<FID, CID, CA, CS, CM> InputCpt<CID, CA, CS, CM> for TextInputCpt<FID>
67where
68    FID: EzCptIds,
69    CID: EzCptIds,
70    CA: EzArgs,
71    CS: EzState,
72    CM: EzMsg,
73{
74    fn get_value(&self) -> FormFieldValue {
75        FormFieldValue::Text(self.input.value().to_string())
76    }
77}
78impl<FID> MockComponent for TextInputCpt<FID>
79where
80    FID: EzCptIds,
81{
82    fn draw(&mut self, area: Rect, buf: &mut Buffer, theme: &Theme) {
83        let width = area.width.max(3) - 3;
84        let scroll = self.input.visual_scroll(width as usize);
85
86        let displayed_text = if self.secret {
87            self.input.value().chars().map(|_| '*').collect()
88        } else {
89            self.input.value().to_string()
90        };
91        Paragraph::new(displayed_text)
92            .scroll((0, u16::try_from(scroll).unwrap_or_default()))
93            .block(theme.block(self.props()).border_type(BorderType::Double))
94            .render(area, buf);
95    }
96}
97
98impl<FID, CID, CA, CS, CM> Component<CID, CA, CS, CM> for TextInputCpt<FID>
99where
100    FID: EzCptIds,
101    CID: EzCptIds,
102    CA: EzArgs,
103    CS: EzState,
104    CM: EzMsg,
105{
106    fn on_event(
107        &mut self,
108        event: EzEvent<CID, CM>,
109        _state: &mut State<CS>,
110    ) -> Vec<EzEvent<CID, CM>> {
111        if let Some(event) = event.try_original() {
112            self.input.handle_event(&event);
113        }
114        vec![]
115    }
116
117    fn focusable(&self) -> bool {
118        true
119    }
120}