mirror of
https://github.com/danbulant/cushy
synced 2026-07-03 02:00:36 +00:00
Validations
This commit is contained in:
parent
f107267409
commit
0fd8a9487f
6 changed files with 335 additions and 64 deletions
|
|
@ -32,6 +32,7 @@ fn main() -> gooey::Result {
|
||||||
username
|
username
|
||||||
.clone()
|
.clone()
|
||||||
.into_input()
|
.into_input()
|
||||||
|
.placeholder("Username")
|
||||||
.validation(username_valid)
|
.validation(username_valid)
|
||||||
.hint("* required"),
|
.hint("* required"),
|
||||||
)
|
)
|
||||||
|
|
@ -43,6 +44,7 @@ fn main() -> gooey::Result {
|
||||||
password
|
password
|
||||||
.clone()
|
.clone()
|
||||||
.into_input()
|
.into_input()
|
||||||
|
.placeholder("Password")
|
||||||
.validation(password_valid)
|
.validation(password_valid)
|
||||||
.hint("* required, 8 characters min"),
|
.hint("* required, 8 characters min"),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
307
src/value.rs
307
src/value.rs
|
|
@ -1,6 +1,5 @@
|
||||||
//! Types for storing and interacting with values in Widgets.
|
//! Types for storing and interacting with values in Widgets.
|
||||||
|
|
||||||
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};
|
||||||
|
|
@ -13,7 +12,6 @@ use std::thread::ThreadId;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use ahash::AHashSet;
|
use ahash::AHashSet;
|
||||||
use intentional::Assert;
|
|
||||||
|
|
||||||
use crate::animation::{DynamicTransition, IntoAnimate, LinearInterpolate, Spawn};
|
use crate::animation::{DynamicTransition, IntoAnimate, LinearInterpolate, Spawn};
|
||||||
use crate::context::sealed::WindowHandle;
|
use crate::context::sealed::WindowHandle;
|
||||||
|
|
@ -78,7 +76,7 @@ impl<T> Dynamic<T> {
|
||||||
RIntoT: FnMut(&R) -> RIntoTResult + Send + 'static,
|
RIntoT: FnMut(&R) -> RIntoTResult + Send + 'static,
|
||||||
{
|
{
|
||||||
let initial_r = self
|
let initial_r = self
|
||||||
.map_ref(&mut t_into_r)
|
.map_ref(|v| t_into_r(v))
|
||||||
.into()
|
.into()
|
||||||
.expect("t_into_r must succeed with the current value");
|
.expect("t_into_r must succeed with the current value");
|
||||||
let r = Dynamic::new(initial_r);
|
let r = Dynamic::new(initial_r);
|
||||||
|
|
@ -122,8 +120,19 @@ impl<T> Dynamic<T> {
|
||||||
/// This function panics if this value is already locked by the current
|
/// This function panics if this value is already locked by the current
|
||||||
/// thread.
|
/// thread.
|
||||||
pub fn map_ref<R>(&self, map: impl FnOnce(&T) -> R) -> R {
|
pub fn map_ref<R>(&self, map: impl FnOnce(&T) -> R) -> R {
|
||||||
|
self.map_generational(|gen| map(&gen.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maps the contents with read-only access, providing access to the value's
|
||||||
|
/// [`Generation`].
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
|
pub fn map_generational<R>(&self, map: impl FnOnce(&GenerationalValue<T>) -> R) -> R {
|
||||||
let state = self.state().expect("deadlocked");
|
let state = self.state().expect("deadlocked");
|
||||||
map(&state.wrapped.value)
|
map(&state.wrapped)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maps the contents with exclusive access. Before returning from this
|
/// Maps the contents with exclusive access. Before returning from this
|
||||||
|
|
@ -186,6 +195,19 @@ impl<T> Dynamic<T> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attaches `for_each` to this value and its [`Generation`] so that it is
|
||||||
|
/// invoked each time the value's contents are updated.
|
||||||
|
pub fn for_each_generational<F>(&self, mut for_each: F)
|
||||||
|
where
|
||||||
|
T: Send + 'static,
|
||||||
|
F: for<'a> FnMut(&'a GenerationalValue<T>) + Send + 'static,
|
||||||
|
{
|
||||||
|
let this = self.clone();
|
||||||
|
self.0.for_each(move || {
|
||||||
|
this.map_generational(&mut for_each);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Attaches `for_each` to this value so that it is invoked each time the
|
/// Attaches `for_each` to this value so that it is invoked each time the
|
||||||
/// value's contents are updated.
|
/// value's contents are updated.
|
||||||
pub fn for_each_cloned<F>(&self, mut for_each: F)
|
pub fn for_each_cloned<F>(&self, mut for_each: F)
|
||||||
|
|
@ -223,6 +245,18 @@ impl<T> Dynamic<T> {
|
||||||
self.0.map_each(move || this.map_ref(&mut map))
|
self.0.map_each(move || this.map_ref(&mut map))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new dynamic value that contains the result of invoking `map`
|
||||||
|
/// each time this value is changed.
|
||||||
|
pub fn map_each_generational<R, F>(&self, mut map: F) -> Dynamic<R>
|
||||||
|
where
|
||||||
|
T: Send + 'static,
|
||||||
|
F: for<'a> FnMut(&'a GenerationalValue<T>) -> R + Send + 'static,
|
||||||
|
R: PartialEq + Send + 'static,
|
||||||
|
{
|
||||||
|
let this = self.clone();
|
||||||
|
self.0.map_each(move || this.map_generational(&mut map))
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a new dynamic value that contains the result of invoking `map`
|
/// Creates a new dynamic value that contains the result of invoking `map`
|
||||||
/// each time this value is changed.
|
/// each time this value is changed.
|
||||||
pub fn map_each_cloned<R, F>(&self, mut map: F) -> Dynamic<R>
|
pub fn map_each_cloned<R, F>(&self, mut map: F) -> Dynamic<R>
|
||||||
|
|
@ -386,9 +420,7 @@ impl<T> Dynamic<T> {
|
||||||
where
|
where
|
||||||
T: PartialEq,
|
T: PartialEq,
|
||||||
{
|
{
|
||||||
let cell = Cell::new(Some(new_value));
|
|
||||||
match self.0.map_mut(|value, changed| {
|
match self.0.map_mut(|value, changed| {
|
||||||
let new_value = cell.take().assert("only one callback will be invoked");
|
|
||||||
if *value == new_value {
|
if *value == new_value {
|
||||||
*changed = false;
|
*changed = false;
|
||||||
Err(ReplaceError::NoChange(new_value))
|
Err(ReplaceError::NoChange(new_value))
|
||||||
|
|
@ -824,10 +856,58 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A value stored in a [`Dynamic`] with its [`Generation`].
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
struct GenerationalValue<T> {
|
pub struct GenerationalValue<T> {
|
||||||
|
/// The stored value.
|
||||||
pub value: T,
|
pub value: T,
|
||||||
pub generation: Generation,
|
generation: Generation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> GenerationalValue<T> {
|
||||||
|
/// Returns the generation of this value.
|
||||||
|
///
|
||||||
|
/// Each time a [`Dynamic`] is updated, the generation is also updated. This
|
||||||
|
/// value can be used to track whether a particular value has been observed.
|
||||||
|
pub const fn generation(&self) -> Generation {
|
||||||
|
self.generation
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new instance containing the result of invoking `map` with
|
||||||
|
/// `self.value`.
|
||||||
|
///
|
||||||
|
/// The returned instance will have the same generation as this instance.
|
||||||
|
pub fn map<U>(self, map: impl FnOnce(T) -> U) -> GenerationalValue<U> {
|
||||||
|
GenerationalValue {
|
||||||
|
value: map(self.value),
|
||||||
|
generation: self.generation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new instance containing the result of invoking `map` with
|
||||||
|
/// `&self.value`.
|
||||||
|
///
|
||||||
|
/// The returned instance will have the same generation as this instance.
|
||||||
|
pub fn map_ref<U>(&self, map: impl for<'a> FnOnce(&'a T) -> U) -> GenerationalValue<U> {
|
||||||
|
GenerationalValue {
|
||||||
|
value: map(&self.value),
|
||||||
|
generation: self.generation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Deref for GenerationalValue<T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DerefMut for GenerationalValue<T> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An exclusive reference to the contents of a [`Dynamic`].
|
/// An exclusive reference to the contents of a [`Dynamic`].
|
||||||
|
|
@ -1695,6 +1775,7 @@ enum ValidationsState {
|
||||||
Initial,
|
Initial,
|
||||||
Resetting,
|
Resetting,
|
||||||
Checked,
|
Checked,
|
||||||
|
Disabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Validations {
|
impl Validations {
|
||||||
|
|
@ -1715,57 +1796,109 @@ impl Validations {
|
||||||
E: Display,
|
E: Display,
|
||||||
{
|
{
|
||||||
let validation = Dynamic::new(Validation::None);
|
let validation = Dynamic::new(Validation::None);
|
||||||
self.invalid.map_mut(|invalid| *invalid += 1);
|
let mut message_mapping = Self::map_to_message(move |value| check(value));
|
||||||
|
let error_message = dynamic.map_each_generational(move |value| message_mapping(value));
|
||||||
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({
|
(&self.state, &error_message).for_each_cloned({
|
||||||
|
let mut f = self.generate_validation(dynamic);
|
||||||
let validation = validation.clone();
|
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)| {
|
move |(current_state, message)| {
|
||||||
let new_status = if let Some(err) = message {
|
validation.set(f(current_state, 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
|
validation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_to_message<T, E, Valid>(
|
||||||
|
mut check: Valid,
|
||||||
|
) -> impl for<'a> FnMut(&'a GenerationalValue<T>) -> GenerationalValue<Option<String>> + Send + 'static
|
||||||
|
where
|
||||||
|
T: Send + 'static,
|
||||||
|
Valid: for<'a> FnMut(&'a T) -> Result<(), E> + Send + 'static,
|
||||||
|
E: Display,
|
||||||
|
{
|
||||||
|
move |value| {
|
||||||
|
value.map_ref(|value| match check(value) {
|
||||||
|
Ok(()) => None,
|
||||||
|
Err(err) => Some(err.to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_validation<T>(
|
||||||
|
&self,
|
||||||
|
dynamic: &Dynamic<T>,
|
||||||
|
) -> impl FnMut(ValidationsState, GenerationalValue<Option<String>>) -> Validation
|
||||||
|
where
|
||||||
|
T: Send + 'static,
|
||||||
|
{
|
||||||
|
self.invalid.map_mut(|invalid| *invalid += 1);
|
||||||
|
|
||||||
|
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, generational| {
|
||||||
|
let new_invalid = match (¤t_state, &generational.value) {
|
||||||
|
(ValidationsState::Disabled, _) | (_, None) => false,
|
||||||
|
(_, Some(_)) => true,
|
||||||
|
};
|
||||||
|
if invalid != new_invalid {
|
||||||
|
if new_invalid {
|
||||||
|
invalid_count.map_mut(|invalid| *invalid += 1);
|
||||||
|
} else {
|
||||||
|
invalid_count.map_mut(|invalid| *invalid -= 1);
|
||||||
|
}
|
||||||
|
invalid = new_invalid;
|
||||||
|
}
|
||||||
|
let new_status = if let Some(err) = generational.value {
|
||||||
|
Validation::Invalid(err.to_string())
|
||||||
|
} else {
|
||||||
|
Validation::Valid
|
||||||
|
};
|
||||||
|
match current_state {
|
||||||
|
ValidationsState::Resetting => {
|
||||||
|
initial_generation = dynamic.generation();
|
||||||
|
let state = state.clone();
|
||||||
|
Duration::ZERO
|
||||||
|
.on_complete(move || {
|
||||||
|
state.set(ValidationsState::Initial);
|
||||||
|
})
|
||||||
|
.launch();
|
||||||
|
Validation::None
|
||||||
|
}
|
||||||
|
ValidationsState::Initial if initial_generation == dynamic.generation() => {
|
||||||
|
Validation::None
|
||||||
|
}
|
||||||
|
_ => new_status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a builder that can be used to create validations that only run
|
||||||
|
/// when `condition` is true.
|
||||||
|
pub fn when(&self, condition: impl IntoDynamic<bool>) -> WhenValidation<'_> {
|
||||||
|
WhenValidation {
|
||||||
|
validations: self,
|
||||||
|
condition: condition.into_dynamic(),
|
||||||
|
not: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a builder that can be used to create validations that only run
|
||||||
|
/// when `condition` is false.
|
||||||
|
pub fn when_not(&self, condition: impl IntoDynamic<bool>) -> WhenValidation<'_> {
|
||||||
|
WhenValidation {
|
||||||
|
validations: self,
|
||||||
|
condition: condition.into_dynamic(),
|
||||||
|
not: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns true if this set of validations are all valid.
|
/// Returns true if this set of validations are all valid.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn is_valid(&self) -> bool {
|
pub fn is_valid(&self) -> bool {
|
||||||
|
|
@ -1812,3 +1945,81 @@ impl Validations {
|
||||||
self.state.set(ValidationsState::Resetting);
|
self.state.set(ValidationsState::Resetting);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A builder for validations that only run when a precondition is met.
|
||||||
|
pub struct WhenValidation<'a> {
|
||||||
|
validations: &'a Validations,
|
||||||
|
condition: Dynamic<bool>,
|
||||||
|
not: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WhenValidation<'_> {
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// Each change to `dynamic` is validated, but the result of the validation
|
||||||
|
/// will be ignored if the required prerequisite isn't met.
|
||||||
|
#[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);
|
||||||
|
let mut map_to_message = Validations::map_to_message(move |value| check(value));
|
||||||
|
let error_message =
|
||||||
|
dynamic.map_each_generational(move |generational| map_to_message(generational));
|
||||||
|
let mut f = self.validations.generate_validation(dynamic);
|
||||||
|
let not = self.not;
|
||||||
|
|
||||||
|
(&self.condition, &self.validations.state, &error_message).map_each_cloned({
|
||||||
|
let validation = validation.clone();
|
||||||
|
move |(condition, state, message)| {
|
||||||
|
let enabled = if not { !condition } else { condition };
|
||||||
|
let state = if enabled {
|
||||||
|
state
|
||||||
|
} else {
|
||||||
|
ValidationsState::Disabled
|
||||||
|
};
|
||||||
|
let result = f(state, message);
|
||||||
|
if enabled {
|
||||||
|
validation.set(result);
|
||||||
|
} else {
|
||||||
|
validation.set(Validation::None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
validation
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a dynamic validation status that is created by transforming the
|
||||||
|
/// `Err` variant of `result` using [`Display`].
|
||||||
|
///
|
||||||
|
/// The validation is linked with `self` such that checking `self`'s
|
||||||
|
/// validation status will include this validation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn validate_result<E>(&self, result: impl IntoDynamic<Result<(), E>>) -> Dynamic<Validation>
|
||||||
|
where
|
||||||
|
E: Display + Send + 'static,
|
||||||
|
{
|
||||||
|
let result = result.into_dynamic();
|
||||||
|
let error_message = result.map_each(move |value| match value {
|
||||||
|
Ok(()) => None,
|
||||||
|
Err(err) => Some(err.to_string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
self.validate(&error_message, |error_message| match error_message {
|
||||||
|
None => Ok(()),
|
||||||
|
Some(message) => Err(message.clone()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ use kludgine::figures::{
|
||||||
use kludgine::shapes::{Shape, StrokeOptions};
|
use kludgine::shapes::{Shape, StrokeOptions};
|
||||||
use kludgine::text::{MeasuredText, Text, TextOrigin};
|
use kludgine::text::{MeasuredText, Text, TextOrigin};
|
||||||
use kludgine::{Color, DrawableExt};
|
use kludgine::{Color, DrawableExt};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
use crate::context::{EventContext, GraphicsContext, LayoutContext};
|
use crate::context::{EventContext, GraphicsContext, LayoutContext};
|
||||||
|
|
@ -37,6 +37,8 @@ const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500);
|
||||||
pub struct Input<Storage> {
|
pub struct Input<Storage> {
|
||||||
/// The value of this widget.
|
/// The value of this widget.
|
||||||
pub value: Dynamic<Storage>,
|
pub value: Dynamic<Storage>,
|
||||||
|
/// The placeholder text to display when no value is present.
|
||||||
|
pub placeholder: Value<String>,
|
||||||
mask_symbol: Value<CowString>,
|
mask_symbol: Value<CowString>,
|
||||||
mask: CowString,
|
mask: CowString,
|
||||||
on_key: Option<Callback<KeyEvent, EventHandling>>,
|
on_key: Option<Callback<KeyEvent, EventHandling>>,
|
||||||
|
|
@ -53,9 +55,11 @@ struct CachedLayout {
|
||||||
color: Color,
|
color: Color,
|
||||||
generation: Generation,
|
generation: Generation,
|
||||||
mask_generation: Option<Generation>,
|
mask_generation: Option<Generation>,
|
||||||
|
placeholder_generation: Option<Generation>,
|
||||||
mask_bytes: usize,
|
mask_bytes: usize,
|
||||||
width: Option<Px>,
|
width: Option<Px>,
|
||||||
measured: MeasuredText<Px>,
|
measured: MeasuredText<Px>,
|
||||||
|
placeholder: MeasuredText<Px>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CachedLayout {
|
impl CachedLayout {
|
||||||
|
|
@ -63,12 +67,14 @@ impl CachedLayout {
|
||||||
&self,
|
&self,
|
||||||
generation: Generation,
|
generation: Generation,
|
||||||
mask_generation: Option<Generation>,
|
mask_generation: Option<Generation>,
|
||||||
|
placeholder_generation: Option<Generation>,
|
||||||
width: Option<Px>,
|
width: Option<Px>,
|
||||||
color: Color,
|
color: Color,
|
||||||
mask_bytes: usize,
|
mask_bytes: usize,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
self.generation == generation
|
self.generation == generation
|
||||||
&& self.mask_generation == mask_generation
|
&& self.mask_generation == mask_generation
|
||||||
|
&& self.placeholder_generation == placeholder_generation
|
||||||
&& self.width == width
|
&& self.width == width
|
||||||
&& self.color == color
|
&& self.color == color
|
||||||
&& self.mask_bytes == mask_bytes
|
&& self.mask_bytes == mask_bytes
|
||||||
|
|
@ -118,6 +124,7 @@ where
|
||||||
.then(|| CowString::from('\u{2022}'))
|
.then(|| CowString::from('\u{2022}'))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into_value(),
|
.into_value(),
|
||||||
|
placeholder: Value::default(),
|
||||||
cache: None,
|
cache: None,
|
||||||
blink_state: BlinkState::default(),
|
blink_state: BlinkState::default(),
|
||||||
selection: SelectionState::default(),
|
selection: SelectionState::default(),
|
||||||
|
|
@ -128,6 +135,13 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the `placeholder` text, which is displayed when the field has an
|
||||||
|
/// empty value.
|
||||||
|
pub fn placeholder(mut self, placeholder: impl IntoValue<String>) -> Self {
|
||||||
|
self.placeholder = placeholder.into_value();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the symbol to use for masking sensitive content to `symbol`.
|
/// Sets the symbol to use for masking sensitive content to `symbol`.
|
||||||
///
|
///
|
||||||
/// Only the first unicode grapheme will be used for the symbol. A warning
|
/// Only the first unicode grapheme will be used for the symbol. A warning
|
||||||
|
|
@ -212,9 +226,14 @@ where
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO remove a full grapheme
|
if let Ok(Some(offset)) = GraphemeCursor::new(cursor.offset, value.as_str().len(), true)
|
||||||
let removed = value.as_string_mut().remove(cursor.offset - 1);
|
.prev_boundary(value.as_str(), 0)
|
||||||
self.selection.cursor.offset -= removed.len_utf8();
|
{
|
||||||
|
value
|
||||||
|
.as_string_mut()
|
||||||
|
.replace_range(offset..cursor.offset, "");
|
||||||
|
self.selection.cursor.offset -= cursor.offset - offset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -537,6 +556,7 @@ where
|
||||||
let (mut cursor, mut selection) = self.selected_range();
|
let (mut cursor, mut selection) = self.selected_range();
|
||||||
let generation = self.value.generation();
|
let generation = self.value.generation();
|
||||||
let mask_generation = self.mask_symbol.generation();
|
let mask_generation = self.mask_symbol.generation();
|
||||||
|
let placeholder_generation = self.placeholder.generation();
|
||||||
let mut mask_bytes = self
|
let mut mask_bytes = self
|
||||||
.mask_symbol
|
.mask_symbol
|
||||||
.map(|sym| sym.graphemes(true).next().map_or(0, str::len));
|
.map(|sym| sym.graphemes(true).next().map_or(0, str::len));
|
||||||
|
|
@ -544,9 +564,16 @@ where
|
||||||
context.invalidate_when_changed(&self.value);
|
context.invalidate_when_changed(&self.value);
|
||||||
match &mut self.cache {
|
match &mut self.cache {
|
||||||
Some(cache)
|
Some(cache)
|
||||||
if cache.is_current(generation, mask_generation, width, color, mask_bytes) => {}
|
if cache.is_current(
|
||||||
|
generation,
|
||||||
|
mask_generation,
|
||||||
|
placeholder_generation,
|
||||||
|
width,
|
||||||
|
color,
|
||||||
|
mask_bytes,
|
||||||
|
) => {}
|
||||||
_ => {
|
_ => {
|
||||||
let (bytes, measured) = self.value.map_ref(|storage| {
|
let (bytes, measured, placeholder, ) = self.value.map_ref(|storage| {
|
||||||
let mut text = storage.as_str();
|
let mut text = storage.as_str();
|
||||||
let mut bytes = text.len();
|
let mut bytes = text.len();
|
||||||
|
|
||||||
|
|
@ -580,16 +607,21 @@ where
|
||||||
if let Some(width) = width {
|
if let Some(width) = width {
|
||||||
text = text.wrap_at(width);
|
text = text.wrap_at(width);
|
||||||
}
|
}
|
||||||
(bytes, context.gfx.measure_text(text))
|
|
||||||
|
let placeholder_color = context.theme().surface.on_color_variant;
|
||||||
|
let placeholder = self.placeholder.map(|placeholder| context.gfx.measure_text(Text::new(placeholder, placeholder_color)));
|
||||||
|
(bytes, context.gfx.measure_text(text), placeholder)
|
||||||
});
|
});
|
||||||
self.cache = Some(CachedLayout {
|
self.cache = Some(CachedLayout {
|
||||||
bytes,
|
bytes,
|
||||||
color,
|
color,
|
||||||
generation,
|
generation,
|
||||||
mask_generation,
|
mask_generation,
|
||||||
|
placeholder_generation,
|
||||||
mask_bytes,
|
mask_bytes,
|
||||||
width,
|
width,
|
||||||
measured,
|
measured,
|
||||||
|
placeholder,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -612,6 +644,7 @@ where
|
||||||
let cache = self.cache.as_ref().expect("always initialized");
|
let cache = self.cache.as_ref().expect("always initialized");
|
||||||
CacheInfo {
|
CacheInfo {
|
||||||
measured: &cache.measured,
|
measured: &cache.measured,
|
||||||
|
placeholder: &cache.placeholder,
|
||||||
bytes: cache.bytes,
|
bytes: cache.bytes,
|
||||||
masked: mask_bytes > 0,
|
masked: mask_bytes > 0,
|
||||||
cursor,
|
cursor,
|
||||||
|
|
@ -877,6 +910,7 @@ where
|
||||||
|
|
||||||
struct CacheInfo<'a> {
|
struct CacheInfo<'a> {
|
||||||
measured: &'a MeasuredText<Px>,
|
measured: &'a MeasuredText<Px>,
|
||||||
|
placeholder: &'a MeasuredText<Px>,
|
||||||
bytes: usize,
|
bytes: usize,
|
||||||
masked: bool,
|
masked: bool,
|
||||||
cursor: Cursor,
|
cursor: Cursor,
|
||||||
|
|
@ -1076,9 +1110,14 @@ where
|
||||||
context.stroke_outline::<Lp>(outline_color, StrokeOptions::default());
|
context.stroke_outline::<Lp>(outline_color, StrokeOptions::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let text = if cache.bytes > 0 {
|
||||||
|
cache.measured
|
||||||
|
} else {
|
||||||
|
cache.placeholder
|
||||||
|
};
|
||||||
context
|
context
|
||||||
.gfx
|
.gfx
|
||||||
.draw_measured_text(cache.measured.translate_by(padding), TextOrigin::TopLeft);
|
.draw_measured_text(text.translate_by(padding), TextOrigin::TopLeft);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
|
|
@ -1092,7 +1131,12 @@ where
|
||||||
|
|
||||||
let cache = self.layout_text(Some(width.into_signed()), &mut context.graphics);
|
let cache = self.layout_text(Some(width.into_signed()), &mut context.graphics);
|
||||||
|
|
||||||
cache.measured.size.into_unsigned() + Size::squared(padding * 2)
|
cache
|
||||||
|
.measured
|
||||||
|
.size
|
||||||
|
.max(cache.placeholder.size)
|
||||||
|
.into_unsigned()
|
||||||
|
+ Size::squared(padding * 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyboard_input(
|
fn keyboard_input(
|
||||||
|
|
|
||||||
|
|
@ -409,9 +409,13 @@ impl Layout {
|
||||||
) -> Size<UPx> {
|
) -> Size<UPx> {
|
||||||
let (space_constraint, other_constraint) = self.orientation.split_size(available);
|
let (space_constraint, other_constraint) = self.orientation.split_size(available);
|
||||||
let available_space = space_constraint.max();
|
let available_space = space_constraint.max();
|
||||||
let gutter_space = gutter.saturating_mul(UPx::new((self.children.len() - 1).cast::<u32>()));
|
let known_gutters = gutter.saturating_mul(UPx::new(
|
||||||
|
(self.children.len() - self.fit_to_content.len())
|
||||||
|
.saturating_sub(1)
|
||||||
|
.cast::<u32>(),
|
||||||
|
));
|
||||||
let allocated_space =
|
let allocated_space =
|
||||||
self.allocated_space.0 + self.allocated_space.1.into_upx(scale).ceil() + gutter_space;
|
self.allocated_space.0 + self.allocated_space.1.into_upx(scale).ceil() + known_gutters;
|
||||||
let mut remaining = available_space.saturating_sub(allocated_space);
|
let mut remaining = available_space.saturating_sub(allocated_space);
|
||||||
// If our `other_constraint` is not known, we will need to give child
|
// If our `other_constraint` is not known, we will need to give child
|
||||||
// widgets an opportunity to lay themselves out in the full area. This
|
// widgets an opportunity to lay themselves out in the full area. This
|
||||||
|
|
@ -421,7 +425,7 @@ impl Layout {
|
||||||
|
|
||||||
// Measure the children that fit their content
|
// Measure the children that fit their content
|
||||||
self.other = UPx::ZERO;
|
self.other = UPx::ZERO;
|
||||||
for &id in &self.fit_to_content {
|
for (fit_index, &id) in self.fit_to_content.iter().enumerate() {
|
||||||
let index = self.children.index_of_id(id).expect("child not found");
|
let index = self.children.index_of_id(id).expect("child not found");
|
||||||
let (measured, other) = self.orientation.split_size(measure(
|
let (measured, other) = self.orientation.split_size(measure(
|
||||||
index,
|
index,
|
||||||
|
|
@ -430,7 +434,16 @@ impl Layout {
|
||||||
!needs_final_layout,
|
!needs_final_layout,
|
||||||
));
|
));
|
||||||
self.layouts[index].size = measured;
|
self.layouts[index].size = measured;
|
||||||
self.other = self.other.max(other);
|
if measured == 0 {
|
||||||
|
self.other = UPx::ZERO;
|
||||||
|
} else {
|
||||||
|
if fit_index < self.fit_to_content.len() - 1
|
||||||
|
|| self.fit_to_content.len() != self.children.len()
|
||||||
|
{
|
||||||
|
remaining = remaining.saturating_sub(gutter);
|
||||||
|
}
|
||||||
|
self.other = self.other.max(other);
|
||||||
|
}
|
||||||
remaining = remaining.saturating_sub(measured);
|
remaining = remaining.saturating_sub(measured);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -495,7 +508,9 @@ impl Layout {
|
||||||
let mut offset = UPx::ZERO;
|
let mut offset = UPx::ZERO;
|
||||||
for index in 0..self.children.len() {
|
for index in 0..self.children.len() {
|
||||||
self.layouts[index].offset = offset;
|
self.layouts[index].offset = offset;
|
||||||
offset += self.layouts[index].size + gutter;
|
if self.layouts[index].size > 0 {
|
||||||
|
offset += self.layouts[index].size + gutter;
|
||||||
|
}
|
||||||
if needs_final_layout {
|
if needs_final_layout {
|
||||||
self.orientation.split_size(measure(
|
self.orientation.split_size(measure(
|
||||||
index,
|
index,
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ impl WrapperWidget for Switcher {
|
||||||
context.remove_child(&removed);
|
context.remove_child(&removed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
context.invalidate_when_changed(&self.source);
|
||||||
available_space
|
available_space
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ impl MakeWidget for Validated {
|
||||||
Value::Dynamic(hint) => (&hint, &self.validation)
|
Value::Dynamic(hint) => (&hint, &self.validation)
|
||||||
.map_each(move |(hint, validation)| validation.message(hint).to_string()),
|
.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 error_color = Dynamic::new(Color::CLEAR_BLACK);
|
||||||
let default_color = Dynamic::new(Color::CLEAR_BLACK);
|
let default_color = Dynamic::new(Color::CLEAR_BLACK);
|
||||||
|
|
@ -75,7 +74,6 @@ impl MakeWidget for Validated {
|
||||||
// TODO these should be components
|
// TODO these should be components
|
||||||
.with(&TextSize, Lp::points(9))
|
.with(&TextSize, Lp::points(9))
|
||||||
.with(&LineHeight, Lp::points(13))
|
.with(&LineHeight, Lp::points(13))
|
||||||
.collapse_vertically(collapse)
|
|
||||||
.align_left(),
|
.align_left(),
|
||||||
)
|
)
|
||||||
.into_rows(),
|
.into_rows(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue