Skip to main content

egui_charts/studies/
inputs.rs

1//! Typed input schemas for built-in and host studies.
2
3pub use crate::model::{InputValue, StudySource};
4
5/// The accepted values of a study input.
6#[derive(Clone, Debug)]
7#[non_exhaustive]
8pub enum InputKind {
9    /// Inclusive integer bounds.
10    Length(usize, usize),
11    /// Inclusive signed integer bounds.
12    Integer(i32, i32),
13    /// Inclusive finite numeric bounds.
14    Number(f64, f64),
15    /// Labels in selection order.
16    Choice(Vec<String>),
17    /// An enabled or disabled input.
18    Toggle,
19    /// A price field or another study's output. The chart checks dependency availability.
20    Source,
21    /// An RGBA colour.
22    Color,
23    /// A calculation interval, or the chart interval.
24    Timeframe,
25    /// A host-resolved symbol, or the chart symbol.
26    Symbol,
27    /// Text on one line.
28    Text,
29}
30
31/// An editable input, with a stable study-local id and its current value.
32#[derive(Clone, Debug)]
33pub struct StudyInput {
34    /// Id passed to [`Indicator::set_input`](super::Indicator::set_input).
35    pub id: String,
36    /// User-facing field label.
37    pub label: String,
38    /// Current value.
39    pub value: InputValue,
40    /// Initial value, retained when the current value is edited.
41    pub default: InputValue,
42    /// Accepted values, used to build the editor.
43    pub kind: InputKind,
44}
45
46impl StudyInput {
47    /// Define an input for a host study.
48    pub fn new(
49        id: impl Into<String>,
50        label: impl Into<String>,
51        value: InputValue,
52        kind: InputKind,
53    ) -> Self {
54        Self {
55            id: id.into(),
56            label: label.into(),
57            default: value.clone(),
58            value,
59            kind,
60        }
61    }
62
63    /// Set the declared default independently of the current value.
64    pub fn with_default(mut self, value: InputValue) -> Self {
65        self.default = value;
66        self
67    }
68
69    /// Check a value against this input's schema. Setters must also validate values.
70    pub fn accepts(&self, value: &InputValue) -> bool {
71        match (&self.kind, value) {
72            (InputKind::Length(min, max), InputValue::Length(v)) => (*min..=*max).contains(v),
73            (InputKind::Integer(min, max), InputValue::Integer(v)) => (*min..=*max).contains(v),
74            (InputKind::Number(min, max), InputValue::Number(v)) => {
75                v.is_finite() && (*min..=*max).contains(v)
76            }
77            (InputKind::Choice(choices), InputValue::Choice(v)) => *v < choices.len(),
78            (InputKind::Toggle, InputValue::Toggle(_))
79            | (InputKind::Source, InputValue::Source(_))
80            | (InputKind::Color, InputValue::Color(_))
81            | (InputKind::Timeframe, InputValue::Timeframe(_)) => true,
82            (InputKind::Symbol, InputValue::Symbol(value))
83            | (InputKind::Text, InputValue::Text(value)) => {
84                value.trim() == value && !value.chars().any(char::is_control)
85            }
86            _ => false,
87        }
88    }
89}
90
91#[cfg(feature = "studies")]
92macro_rules! input_spec {
93    ($id:expr, $value:expr, integer($label:expr, $min:expr, $max:expr)) => {
94        crate::studies::StudyInput::new(
95            $id,
96            $label,
97            crate::studies::InputValue::Integer($value),
98            crate::studies::InputKind::Integer($min, $max),
99        )
100    };
101    ($id:expr, $value:expr, length($label:expr, $min:expr, $max:expr)) => {
102        crate::studies::StudyInput::new(
103            $id,
104            $label,
105            crate::studies::InputValue::Length($value),
106            crate::studies::InputKind::Length($min, $max),
107        )
108    };
109    ($id:expr, $value:expr, number($label:expr, $min:expr, $max:expr)) => {
110        crate::studies::StudyInput::new(
111            $id,
112            $label,
113            crate::studies::InputValue::Number($value as f64),
114            crate::studies::InputKind::Number($min, $max),
115        )
116    };
117    ($id:expr, $value:expr, choice($label:expr, $choices:expr)) => {{
118        let choices = $choices;
119        crate::studies::StudyInput::new(
120            $id,
121            $label,
122            crate::studies::InputValue::Choice(
123                choices.iter().position(|v| *v == $value).unwrap_or(0),
124            ),
125            crate::studies::InputKind::Choice(choices.iter().map(|v| format!("{v:?}")).collect()),
126        )
127    }};
128    ($id:expr, $value:expr, toggle($label:expr)) => {
129        crate::studies::StudyInput::new(
130            $id,
131            $label,
132            crate::studies::InputValue::Toggle($value),
133            crate::studies::InputKind::Toggle,
134        )
135    };
136    ($id:expr, $value:expr, color($label:expr)) => {
137        crate::studies::StudyInput::new(
138            $id,
139            $label,
140            crate::studies::InputValue::Color($value),
141            crate::studies::InputKind::Color,
142        )
143    };
144}
145
146#[cfg(feature = "studies")]
147macro_rules! input_assign {
148    ($value:expr, integer($($args:expr),*)) => {
149        match $value {
150            crate::studies::InputValue::Integer(v) => v,
151            _ => unreachable!(),
152        }
153    };
154    ($value:expr, length($($args:expr),*)) => {
155        match $value {
156            crate::studies::InputValue::Length(v) => v,
157            _ => unreachable!(),
158        }
159    };
160    ($value:expr, number($($args:expr),*)) => {
161        match $value {
162            crate::studies::InputValue::Number(v) => v as _,
163            _ => unreachable!(),
164        }
165    };
166    ($value:expr, choice($label:expr, $choices:expr)) => {
167        match $value {
168            crate::studies::InputValue::Choice(v) => $choices
169                .into_iter()
170                .nth(v)
171                .expect("validated study input choice"),
172            _ => unreachable!(),
173        }
174    };
175    ($value:expr, toggle($label:expr)) => {
176        match $value {
177            crate::studies::InputValue::Toggle(v) => v,
178            _ => unreachable!(),
179        }
180    };
181    ($value:expr, color($label:expr)) => {
182        match $value {
183            crate::studies::InputValue::Color(v) => v,
184            _ => unreachable!(),
185        }
186    };
187}
188
189#[cfg(feature = "studies")]
190macro_rules! study_inputs {
191    ($this:ident; $($field:ident $([$index:literal])? : $kind:ident($($args:expr),*)),+ $(,)? ; $($changed:ident =>)? $after:block) => {
192        fn inputs(&$this) -> Vec<crate::studies::StudyInput> {
193            let defaults = Self::default();
194            vec![$(crate::studies::inputs::input_spec!(stringify!($field $([$index])?), $this.$field $([$index])?, $kind($($args),*))
195                .with_default(crate::studies::inputs::input_spec!(stringify!($field $([$index])?), defaults.$field $([$index])?, $kind($($args),*)).value)),+]
196        }
197
198        fn set_input(&mut $this, id: &str, value: crate::studies::InputValue) -> Result<(), crate::studies::StudyError> {
199            let schema = $this.inputs().into_iter().find(|input| input.id == id).ok_or_else(|| crate::studies::StudyError::UnknownInput(id.into()))?;
200            if !schema.accepts(&value) { return Err(crate::studies::StudyError::invalid_input(id)); }
201            match id {
202                $(stringify!($field $([$index])?) => { $this.$field $([$index])? = crate::studies::inputs::input_assign!(value, $kind($($args),*)); }),+
203                _ => return Err(crate::studies::StudyError::UnknownInput(id.into())),
204            }
205            $(let $changed = id;)?
206            $after
207            $this.calculate(&[]);
208            Ok(())
209        }
210    };
211}
212
213#[cfg(feature = "studies")]
214pub(crate) use {input_assign, input_spec, study_inputs};