mirror of
https://github.com/danbulant/cushy
synced 2026-07-06 11:40:37 +00:00
Validations
This commit is contained in:
parent
b2fdf06e60
commit
f107267409
7 changed files with 419 additions and 25 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
|
||||||
use gooey::value::{Dynamic, MapEach};
|
use gooey::value::{Dynamic, Validations};
|
||||||
use gooey::widget::MakeWidget;
|
use gooey::widget::MakeWidget;
|
||||||
use gooey::widgets::input::{InputValue, MaskedString};
|
use gooey::widgets::input::{InputValue, MaskedString};
|
||||||
use gooey::widgets::Expand;
|
use gooey::widgets::Expand;
|
||||||
|
|
@ -10,19 +10,42 @@ use kludgine::figures::units::Lp;
|
||||||
fn main() -> gooey::Result {
|
fn main() -> gooey::Result {
|
||||||
let username = Dynamic::default();
|
let username = Dynamic::default();
|
||||||
let password = Dynamic::default();
|
let password = Dynamic::default();
|
||||||
|
let validations = Validations::default();
|
||||||
|
|
||||||
let valid =
|
let username_valid = validations.validate(&username, |u: &String| {
|
||||||
(&username, &password).map_each(|(username, password)| validate(username, password));
|
if u.is_empty() {
|
||||||
|
Err("usernames must contain at least one character")
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let password_valid = validations.validate(&password, |u: &MaskedString| match u.len() {
|
||||||
|
0..=7 => Err("passwords must be at least 8 characters long"),
|
||||||
|
_ => Ok(()),
|
||||||
|
});
|
||||||
|
|
||||||
// TODO this should be a grid layout to ensure proper visual alignment.
|
// TODO this should be a grid layout to ensure proper visual alignment.
|
||||||
let username_field = "Username"
|
let username_field = "Username"
|
||||||
.align_left()
|
.align_left()
|
||||||
.and(username.clone().into_input())
|
.and(
|
||||||
|
username
|
||||||
|
.clone()
|
||||||
|
.into_input()
|
||||||
|
.validation(username_valid)
|
||||||
|
.hint("* required"),
|
||||||
|
)
|
||||||
.into_rows();
|
.into_rows();
|
||||||
|
|
||||||
let password_field = "Password"
|
let password_field = "Password"
|
||||||
.align_left()
|
.align_left()
|
||||||
.and(password.clone().into_input())
|
.and(
|
||||||
|
password
|
||||||
|
.clone()
|
||||||
|
.into_input()
|
||||||
|
.validation(password_valid)
|
||||||
|
.hint("* required, 8 characters min"),
|
||||||
|
)
|
||||||
.into_rows();
|
.into_rows();
|
||||||
|
|
||||||
let buttons = "Cancel"
|
let buttons = "Cancel"
|
||||||
|
|
@ -36,12 +59,11 @@ fn main() -> gooey::Result {
|
||||||
.and(
|
.and(
|
||||||
"Log In"
|
"Log In"
|
||||||
.into_button()
|
.into_button()
|
||||||
.on_click(move |_| {
|
.on_click(validations.when_valid(move |()| {
|
||||||
println!("Welcome, {}", username.get());
|
println!("Welcome, {}", username.get());
|
||||||
exit(0);
|
exit(0);
|
||||||
})
|
}))
|
||||||
.into_default()
|
.into_default(),
|
||||||
.with_enabled(valid),
|
|
||||||
)
|
)
|
||||||
.into_columns();
|
.into_columns();
|
||||||
|
|
||||||
|
|
@ -57,7 +79,3 @@ fn main() -> gooey::Result {
|
||||||
.expand()
|
.expand()
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(username: &String, password: &MaskedString) -> bool {
|
|
||||||
!username.is_empty() && !password.is_empty()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
52
examples/validation.rs
Normal file
52
examples/validation.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
use gooey::value::{Dynamic, Validations};
|
||||||
|
use gooey::widget::MakeWidget;
|
||||||
|
use gooey::widgets::input::InputValue;
|
||||||
|
use gooey::Run;
|
||||||
|
use kludgine::figures::units::Lp;
|
||||||
|
|
||||||
|
fn main() -> gooey::Result {
|
||||||
|
let text = Dynamic::default();
|
||||||
|
let validations = Validations::default();
|
||||||
|
|
||||||
|
"Hinted"
|
||||||
|
.and(
|
||||||
|
text.clone()
|
||||||
|
.into_input()
|
||||||
|
.validation(validations.validate(&text, validate_input))
|
||||||
|
.hint("* required"),
|
||||||
|
)
|
||||||
|
.and("Not Hinted")
|
||||||
|
.and(
|
||||||
|
text.clone()
|
||||||
|
.into_input()
|
||||||
|
.validation(validations.validate(&text, validate_input)),
|
||||||
|
)
|
||||||
|
.and(
|
||||||
|
"Submit"
|
||||||
|
.into_button()
|
||||||
|
.on_click(validations.clone().when_valid(move |()| {
|
||||||
|
println!(
|
||||||
|
"Success! This callback only happens when all associated validations are valid"
|
||||||
|
);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.and("Reset".into_button().on_click(move |()| {
|
||||||
|
let _value = text.take();
|
||||||
|
validations.reset();
|
||||||
|
}))
|
||||||
|
.into_rows()
|
||||||
|
.width(Lp::inches(6))
|
||||||
|
.centered()
|
||||||
|
.expand()
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_input(input: &String) -> Result<(), &'static str> {
|
||||||
|
if input.is_empty() {
|
||||||
|
Err("This field cannot be empty")
|
||||||
|
} else if input.trim().is_empty() {
|
||||||
|
Err("This field must have at least one non-whitespace character")
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
220
src/value.rs
220
src/value.rs
|
|
@ -4,7 +4,9 @@ use std::cell::Cell;
|
||||||
use std::fmt::{Debug, Display};
|
use std::fmt::{Debug, Display};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::ops::{Deref, DerefMut, Not};
|
use std::ops::{Deref, DerefMut, Not};
|
||||||
|
use std::panic::UnwindSafe;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::sync::atomic::{self, AtomicBool};
|
||||||
use std::sync::{Arc, Mutex, MutexGuard, TryLockError};
|
use std::sync::{Arc, Mutex, MutexGuard, TryLockError};
|
||||||
use std::task::{Poll, Waker};
|
use std::task::{Poll, Waker};
|
||||||
use std::thread::ThreadId;
|
use std::thread::ThreadId;
|
||||||
|
|
@ -495,6 +497,28 @@ impl<T> Dynamic<T> {
|
||||||
{
|
{
|
||||||
Radio::new(widget_value, self.clone(), label)
|
Radio::new(widget_value, self.clone(), label)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validates the contents of this dynamic using the `check` function,
|
||||||
|
/// returning a dynamic that contains the validation status.
|
||||||
|
#[must_use]
|
||||||
|
pub fn validate_with<E, Valid>(&self, mut check: Valid) -> Dynamic<Validation>
|
||||||
|
where
|
||||||
|
T: Send + 'static,
|
||||||
|
Valid: for<'a> FnMut(&'a T) -> Result<(), E> + Send + 'static,
|
||||||
|
E: Display,
|
||||||
|
{
|
||||||
|
let validation = Dynamic::new(Validation::None);
|
||||||
|
self.for_each({
|
||||||
|
let validation = validation.clone();
|
||||||
|
move |value| {
|
||||||
|
validation.set(match check(value) {
|
||||||
|
Ok(()) => Validation::Valid,
|
||||||
|
Err(err) => Validation::Invalid(err.to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
validation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Dynamic<WidgetInstance> {
|
impl Dynamic<WidgetInstance> {
|
||||||
|
|
@ -670,7 +694,7 @@ impl<T> DynamicData<T> {
|
||||||
F: for<'a> FnMut() + Send + 'static,
|
F: for<'a> FnMut() + Send + 'static,
|
||||||
{
|
{
|
||||||
let state = self.state().expect("deadlocked");
|
let state = self.state().expect("deadlocked");
|
||||||
let mut callbacks = state.callbacks.lock().ignore_poison();
|
let mut callbacks = state.callbacks.callbacks.lock().ignore_poison();
|
||||||
callbacks.push(Box::new(map));
|
callbacks.push(Box::new(map));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -716,7 +740,7 @@ impl Display for DeadlockError {
|
||||||
|
|
||||||
struct State<T> {
|
struct State<T> {
|
||||||
wrapped: GenerationalValue<T>,
|
wrapped: GenerationalValue<T>,
|
||||||
callbacks: Arc<Mutex<Vec<Box<dyn ValueCallback>>>>,
|
callbacks: Arc<ChangeCallbacksData>,
|
||||||
windows: AHashSet<WindowHandle>,
|
windows: AHashSet<WindowHandle>,
|
||||||
widgets: AHashSet<(WindowHandle, WidgetId)>,
|
widgets: AHashSet<(WindowHandle, WidgetId)>,
|
||||||
wakers: Vec<Waker>,
|
wakers: Vec<Waker>,
|
||||||
|
|
@ -753,14 +777,36 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChangeCallbacks(Arc<Mutex<Vec<Box<dyn ValueCallback>>>>);
|
#[derive(Default)]
|
||||||
|
struct ChangeCallbacksData {
|
||||||
|
callbacks: Mutex<Vec<Box<dyn ValueCallback>>>,
|
||||||
|
currently_executing: AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChangeCallbacks(Arc<ChangeCallbacksData>);
|
||||||
|
|
||||||
impl Drop for ChangeCallbacks {
|
impl Drop for ChangeCallbacks {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Ok(mut callbacks) = self.0.lock() {
|
if self
|
||||||
|
.0
|
||||||
|
.currently_executing
|
||||||
|
.compare_exchange(
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
atomic::Ordering::Release,
|
||||||
|
atomic::Ordering::Acquire,
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
let mut callbacks = self.0.callbacks.lock().ignore_poison();
|
||||||
for callback in &mut *callbacks {
|
for callback in &mut *callbacks {
|
||||||
callback.changed();
|
callback.changed();
|
||||||
}
|
}
|
||||||
|
self.0
|
||||||
|
.currently_executing
|
||||||
|
.store(false, atomic::Ordering::Release);
|
||||||
|
} else {
|
||||||
|
tracing::warn!("Could not invoke dynamic callbacks because they are already running on this thread");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1600,3 +1646,169 @@ macro_rules! impl_tuple_map_each_cloned {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_all_tuples!(impl_tuple_map_each_cloned);
|
impl_all_tuples!(impl_tuple_map_each_cloned);
|
||||||
|
|
||||||
|
/// The status of validating data.
|
||||||
|
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
||||||
|
pub enum Validation {
|
||||||
|
/// No validation has been performed yet.
|
||||||
|
///
|
||||||
|
/// This status represents that the data is still in its initial state, so
|
||||||
|
/// errors should be delayed until it is changed.
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
/// The data is valid.
|
||||||
|
Valid,
|
||||||
|
/// The data is invalid. The string contains a human-readable message.
|
||||||
|
Invalid(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validation {
|
||||||
|
/// Returns the effective text to display along side the field.
|
||||||
|
///
|
||||||
|
/// When there is a validation error, it is returned, otherwise the hint is
|
||||||
|
/// returned.
|
||||||
|
#[must_use]
|
||||||
|
pub fn message<'a>(&'a self, hint: &'a str) -> &'a str {
|
||||||
|
match self {
|
||||||
|
Validation::None | Validation::Valid => hint,
|
||||||
|
Validation::Invalid(err) => err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if there is a validation error.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn is_error(&self) -> bool {
|
||||||
|
matches!(self, Self::Invalid(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A grouping of validations that can be checked simultaneously.
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Validations {
|
||||||
|
state: Dynamic<ValidationsState>,
|
||||||
|
invalid: Dynamic<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Eq, PartialEq, Clone)]
|
||||||
|
enum ValidationsState {
|
||||||
|
#[default]
|
||||||
|
Initial,
|
||||||
|
Resetting,
|
||||||
|
Checked,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validations {
|
||||||
|
/// Validates `dynamic`'s contents using `check`, returning a dynamic
|
||||||
|
/// containing the validation status.
|
||||||
|
///
|
||||||
|
/// The validation is linked with `self` such that checking `self`'s
|
||||||
|
/// validation status will include this validation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn validate<T, E, Valid>(
|
||||||
|
&self,
|
||||||
|
dynamic: &Dynamic<T>,
|
||||||
|
mut check: Valid,
|
||||||
|
) -> Dynamic<Validation>
|
||||||
|
where
|
||||||
|
T: Send + 'static,
|
||||||
|
Valid: for<'a> FnMut(&'a T) -> Result<(), E> + Send + 'static,
|
||||||
|
E: Display,
|
||||||
|
{
|
||||||
|
let validation = Dynamic::new(Validation::None);
|
||||||
|
self.invalid.map_mut(|invalid| *invalid += 1);
|
||||||
|
|
||||||
|
let error_message = dynamic.map_each(move |value| match check(value) {
|
||||||
|
Ok(()) => None,
|
||||||
|
Err(err) => Some(err.to_string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
(&self.state, &error_message).for_each_cloned({
|
||||||
|
let validation = validation.clone();
|
||||||
|
let invalid_count = self.invalid.clone();
|
||||||
|
let state = self.state.clone();
|
||||||
|
let dynamic = dynamic.clone();
|
||||||
|
let mut initial_generation = dynamic.generation();
|
||||||
|
let mut invalid = true;
|
||||||
|
|
||||||
|
move |(current_state, message)| {
|
||||||
|
let new_status = if let Some(err) = message {
|
||||||
|
if !invalid {
|
||||||
|
invalid_count.map_mut(|invalid| *invalid += 1);
|
||||||
|
invalid = true;
|
||||||
|
}
|
||||||
|
Validation::Invalid(err.to_string())
|
||||||
|
} else {
|
||||||
|
if invalid {
|
||||||
|
invalid_count.map_mut(|invalid| *invalid -= 1);
|
||||||
|
invalid = false;
|
||||||
|
}
|
||||||
|
Validation::Valid
|
||||||
|
};
|
||||||
|
match current_state {
|
||||||
|
ValidationsState::Resetting => {
|
||||||
|
initial_generation = dynamic.generation();
|
||||||
|
let state = state.clone();
|
||||||
|
validation.set(Validation::None);
|
||||||
|
Duration::ZERO
|
||||||
|
.on_complete(move || {
|
||||||
|
state.set(ValidationsState::Initial);
|
||||||
|
})
|
||||||
|
.launch();
|
||||||
|
}
|
||||||
|
ValidationsState::Initial if initial_generation == dynamic.generation() => {}
|
||||||
|
_ => {
|
||||||
|
validation.set(new_status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
validation
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this set of validations are all valid.
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
self.invoke_callback((), &mut |()| true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invoke_callback<T, R, F>(&self, t: T, handler: &mut F) -> R
|
||||||
|
where
|
||||||
|
F: FnMut(T) -> R + UnwindSafe + Send + 'static,
|
||||||
|
R: Default,
|
||||||
|
{
|
||||||
|
let mut state = self.state.lock();
|
||||||
|
if let ValidationsState::Initial = &*state {
|
||||||
|
*state = ValidationsState::Checked;
|
||||||
|
}
|
||||||
|
drop(state);
|
||||||
|
if self.invalid.get() == 0 {
|
||||||
|
handler(t)
|
||||||
|
} else {
|
||||||
|
R::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a function that invokes `handler` only when all tracked
|
||||||
|
/// validations are valid.
|
||||||
|
///
|
||||||
|
/// The returned function can be use in a
|
||||||
|
/// [`Callback`](crate::widget::Callback).
|
||||||
|
///
|
||||||
|
/// When the contents are invalid, `R::default()` is returned.
|
||||||
|
pub fn when_valid<T, R, F>(
|
||||||
|
self,
|
||||||
|
mut handler: F,
|
||||||
|
) -> impl FnMut(T) -> R + UnwindSafe + Send + 'static
|
||||||
|
where
|
||||||
|
F: FnMut(T) -> R + UnwindSafe + Send + 'static,
|
||||||
|
R: Default,
|
||||||
|
{
|
||||||
|
move |t: T| self.invoke_callback(t, &mut handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the validation status for all related validations.
|
||||||
|
pub fn reset(&self) {
|
||||||
|
self.state.set(ValidationsState::Resetting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,11 @@ use crate::styles::{
|
||||||
};
|
};
|
||||||
use crate::tree::Tree;
|
use crate::tree::Tree;
|
||||||
use crate::utils::IgnorePoison;
|
use crate::utils::IgnorePoison;
|
||||||
use crate::value::{IntoDynamic, IntoValue, Value};
|
use crate::value::{IntoDynamic, IntoValue, Validation, Value};
|
||||||
use crate::widgets::checkbox::{Checkable, CheckboxState};
|
use crate::widgets::checkbox::{Checkable, CheckboxState};
|
||||||
use crate::widgets::{
|
use crate::widgets::{
|
||||||
Align, Button, Checkbox, Collapse, Container, Expand, Resize, Scroll, Space, Stack, Style,
|
Align, Button, Checkbox, Collapse, Container, Expand, Resize, Scroll, Space, Stack, Style,
|
||||||
Themed, ThemedMode,
|
Themed, ThemedMode, Validated,
|
||||||
};
|
};
|
||||||
use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior};
|
use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior};
|
||||||
use crate::{ConstraintLimit, Run};
|
use crate::{ConstraintLimit, Run};
|
||||||
|
|
@ -882,6 +882,11 @@ pub trait MakeWidget: Sized {
|
||||||
fn collapse_vertically(self, collapse_when: impl IntoDynamic<bool>) -> Collapse {
|
fn collapse_vertically(self, collapse_when: impl IntoDynamic<bool>) -> Collapse {
|
||||||
Collapse::vertical(collapse_when, self)
|
Collapse::vertical(collapse_when, self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a widget that shows validation errors and/or hints.
|
||||||
|
fn validation(self, validation: impl IntoDynamic<Validation>) -> Validated {
|
||||||
|
Validated::new(validation, self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A type that can create a [`WidgetInstance`] with a preallocated
|
/// A type that can create a [`WidgetInstance`] with a preallocated
|
||||||
|
|
@ -921,12 +926,6 @@ impl MakeWidget for Color {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MakeWidget for () {
|
|
||||||
fn make_widget(self) -> WidgetInstance {
|
|
||||||
Space::clear().make_widget()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A type that represents whether an event has been handled or ignored.
|
/// A type that represents whether an event has been handled or ignored.
|
||||||
pub type EventHandling = ControlFlow<EventHandled, EventIgnored>;
|
pub type EventHandling = ControlFlow<EventHandled, EventIgnored>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ mod style;
|
||||||
mod switcher;
|
mod switcher;
|
||||||
mod themed;
|
mod themed;
|
||||||
mod tilemap;
|
mod tilemap;
|
||||||
|
mod validated;
|
||||||
|
|
||||||
pub use align::Align;
|
pub use align::Align;
|
||||||
pub use button::Button;
|
pub use button::Button;
|
||||||
|
|
@ -47,3 +48,4 @@ pub use style::Style;
|
||||||
pub use switcher::Switcher;
|
pub use switcher::Switcher;
|
||||||
pub use themed::Themed;
|
pub use themed::Themed;
|
||||||
pub use tilemap::TileMap;
|
pub use tilemap::TileMap;
|
||||||
|
pub use validated::Validated;
|
||||||
|
|
|
||||||
|
|
@ -575,6 +575,7 @@ where
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context.apply_current_font_settings();
|
||||||
let mut text = Text::new(text, color);
|
let mut text = Text::new(text, color);
|
||||||
if let Some(width) = width {
|
if let Some(width) = width {
|
||||||
text = text.wrap_at(width);
|
text = text.wrap_at(width);
|
||||||
|
|
|
||||||
110
src/widgets/validated.rs
Normal file
110
src/widgets/validated.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use kludgine::figures::units::Lp;
|
||||||
|
use kludgine::Color;
|
||||||
|
|
||||||
|
use crate::styles::components::{LineHeight, OutlineColor, TextColor, TextSize};
|
||||||
|
use crate::value::{Dynamic, IntoDynamic, IntoValue, MapEach, Validation, Value};
|
||||||
|
use crate::widget::{MakeWidget, 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 MakeWidget for Validated {
|
||||||
|
fn make_widget(self) -> 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 collapse = message.map_each(String::is_empty);
|
||||||
|
|
||||||
|
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)
|
||||||
|
// TODO these should be components
|
||||||
|
.with(&TextSize, Lp::points(9))
|
||||||
|
.with(&LineHeight, Lp::points(13))
|
||||||
|
.collapse_vertically(collapse)
|
||||||
|
.align_left(),
|
||||||
|
)
|
||||||
|
.into_rows(),
|
||||||
|
),
|
||||||
|
error_color,
|
||||||
|
default_color,
|
||||||
|
}
|
||||||
|
.make_widget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<'_, '_, '_, '_, '_>,
|
||||||
|
) {
|
||||||
|
// TODO move these to components.
|
||||||
|
self.error_color.set(context.theme().error.color);
|
||||||
|
self.default_color.set(context.theme().surface.outline);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue