1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//! A widget that displays the result of validation.

use std::fmt::Debug;

use kludgine::Color;

use crate::styles::components::{
    ErrorColor, LineHeight, LineHeight2, OutlineColor, TextColor, TextSize, TextSize2,
};
use crate::styles::Dimension;
use crate::value::{
    Destination, Dynamic, IntoDynamic, IntoValue, MapEach, Source, Validation, Value,
};
use crate::widget::{MakeWidget, MakeWidgetWithTag, WidgetInstance, WidgetRef, WrapperWidget};

/// A widget that displays validation information around another widget.
///
/// This widget overrides the outline color of its child to be the theme's error
/// color.
///
/// Additionally, a message may be shown below the content widget. If there is a
/// validation error, it is shown. Otherwise, an optional hint message is
/// supported.
#[derive(Debug)]
pub struct Validated {
    hint: Value<String>,
    validation: Dynamic<Validation>,
    validated: WidgetInstance,
}

impl Validated {
    /// Returns a widget that displays validation information around `validated`
    /// based on `validation`.
    #[must_use]
    pub fn new(validation: impl IntoDynamic<Validation>, validated: impl MakeWidget) -> Self {
        Self {
            validation: validation.into_dynamic(),
            validated: validated.make_widget(),
            hint: Value::default(),
        }
    }

    /// Sets the hint message to be displayed when there is no validation error.
    #[must_use]
    pub fn hint(mut self, hint: impl IntoValue<String>) -> Self {
        self.hint = hint.into_value();
        self
    }
}

impl MakeWidgetWithTag for Validated {
    fn make_with_tag(self, id: crate::widget::WidgetTag) -> WidgetInstance {
        let message = match self.hint {
            Value::Constant(hint) => self
                .validation
                .map_each(move |validation| validation.message(&hint).to_string()),
            Value::Dynamic(hint) => (&hint, &self.validation)
                .map_each(move |(hint, validation)| validation.message(hint).to_string()),
        };

        let error_color = Dynamic::new(Color::CLEAR_BLACK);
        let default_color = Dynamic::new(Color::CLEAR_BLACK);
        let color = (&self.validation, &error_color, &default_color).map_each(
            |(validation, error, default)| {
                if validation.is_error() {
                    *error
                } else {
                    *default
                }
            },
        );

        ValidatedWidget {
            contents: WidgetRef::new(
                self.validated
                    .with(&OutlineColor, color.clone())
                    .and(
                        message
                            .with(&TextColor, color)
                            .with_dynamic(&TextSize, ValidatedTextSize)
                            .with_dynamic(&LineHeight, ValidatedLineHeight)
                            .align_left(),
                    )
                    .into_rows(),
            ),
            error_color,
            default_color,
        }
        .make_with_tag(id)
    }
}

#[derive(Debug)]
struct ValidatedWidget {
    contents: WidgetRef,
    error_color: Dynamic<Color>,
    default_color: Dynamic<Color>,
}

impl WrapperWidget for ValidatedWidget {
    fn child_mut(&mut self) -> &mut WidgetRef {
        &mut self.contents
    }

    fn redraw_background(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_>) {
        self.error_color.set(context.get(&InvalidTextColor));
        self.default_color.set(context.get(&HintTextColor));
    }
}

define_components! {
    Validated {
        /// The color of the hint text.
        HintTextColor(Color, "hint_color", @OutlineColor)
        /// The color of invalid text.
        InvalidTextColor(Color, "invalid_color", @ErrorColor)
        /// The text size for the validation message in a [`Validated`] widget.
        ValidatedTextSize(Dimension, "text_size", @TextSize2)
        /// The line hgiht for the validation message in a [`Validated`] widget.
        ValidatedLineHeight(Dimension, "line_height", @LineHeight2)
    }
}