mirror of
https://github.com/danbulant/cushy
synced 2026-06-10 18:13:48 +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
|
||||
.clone()
|
||||
.into_input()
|
||||
.placeholder("Username")
|
||||
.validation(username_valid)
|
||||
.hint("* required"),
|
||||
)
|
||||
|
|
@ -43,6 +44,7 @@ fn main() -> gooey::Result {
|
|||
password
|
||||
.clone()
|
||||
.into_input()
|
||||
.placeholder("Password")
|
||||
.validation(password_valid)
|
||||
.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.
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::future::Future;
|
||||
use std::ops::{Deref, DerefMut, Not};
|
||||
|
|
@ -13,7 +12,6 @@ use std::thread::ThreadId;
|
|||
use std::time::Duration;
|
||||
|
||||
use ahash::AHashSet;
|
||||
use intentional::Assert;
|
||||
|
||||
use crate::animation::{DynamicTransition, IntoAnimate, LinearInterpolate, Spawn};
|
||||
use crate::context::sealed::WindowHandle;
|
||||
|
|
@ -78,7 +76,7 @@ impl<T> Dynamic<T> {
|
|||
RIntoT: FnMut(&R) -> RIntoTResult + Send + 'static,
|
||||
{
|
||||
let initial_r = self
|
||||
.map_ref(&mut t_into_r)
|
||||
.map_ref(|v| t_into_r(v))
|
||||
.into()
|
||||
.expect("t_into_r must succeed with the current value");
|
||||
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
|
||||
/// thread.
|
||||
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");
|
||||
map(&state.wrapped.value)
|
||||
map(&state.wrapped)
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// value's contents are updated.
|
||||
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))
|
||||
}
|
||||
|
||||
/// 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`
|
||||
/// each time this value is changed.
|
||||
pub fn map_each_cloned<R, F>(&self, mut map: F) -> Dynamic<R>
|
||||
|
|
@ -386,9 +420,7 @@ impl<T> Dynamic<T> {
|
|||
where
|
||||
T: PartialEq,
|
||||
{
|
||||
let cell = Cell::new(Some(new_value));
|
||||
match self.0.map_mut(|value, changed| {
|
||||
let new_value = cell.take().assert("only one callback will be invoked");
|
||||
if *value == new_value {
|
||||
*changed = false;
|
||||
Err(ReplaceError::NoChange(new_value))
|
||||
|
|
@ -824,10 +856,58 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// A value stored in a [`Dynamic`] with its [`Generation`].
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct GenerationalValue<T> {
|
||||
pub struct GenerationalValue<T> {
|
||||
/// The stored value.
|
||||
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`].
|
||||
|
|
@ -1695,6 +1775,7 @@ enum ValidationsState {
|
|||
Initial,
|
||||
Resetting,
|
||||
Checked,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl Validations {
|
||||
|
|
@ -1715,57 +1796,109 @@ impl Validations {
|
|||
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()),
|
||||
});
|
||||
let mut message_mapping = Self::map_to_message(move |value| check(value));
|
||||
let error_message = dynamic.map_each_generational(move |value| message_mapping(value));
|
||||
|
||||
(&self.state, &error_message).for_each_cloned({
|
||||
let mut f = self.generate_validation(dynamic);
|
||||
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.set(f(current_state, message));
|
||||
}
|
||||
});
|
||||
|
||||
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.
|
||||
#[must_use]
|
||||
pub fn is_valid(&self) -> bool {
|
||||
|
|
@ -1812,3 +1945,81 @@ impl Validations {
|
|||
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::text::{MeasuredText, Text, TextOrigin};
|
||||
use kludgine::{Color, DrawableExt};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::context::{EventContext, GraphicsContext, LayoutContext};
|
||||
|
|
@ -37,6 +37,8 @@ const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500);
|
|||
pub struct Input<Storage> {
|
||||
/// The value of this widget.
|
||||
pub value: Dynamic<Storage>,
|
||||
/// The placeholder text to display when no value is present.
|
||||
pub placeholder: Value<String>,
|
||||
mask_symbol: Value<CowString>,
|
||||
mask: CowString,
|
||||
on_key: Option<Callback<KeyEvent, EventHandling>>,
|
||||
|
|
@ -53,9 +55,11 @@ struct CachedLayout {
|
|||
color: Color,
|
||||
generation: Generation,
|
||||
mask_generation: Option<Generation>,
|
||||
placeholder_generation: Option<Generation>,
|
||||
mask_bytes: usize,
|
||||
width: Option<Px>,
|
||||
measured: MeasuredText<Px>,
|
||||
placeholder: MeasuredText<Px>,
|
||||
}
|
||||
|
||||
impl CachedLayout {
|
||||
|
|
@ -63,12 +67,14 @@ impl CachedLayout {
|
|||
&self,
|
||||
generation: Generation,
|
||||
mask_generation: Option<Generation>,
|
||||
placeholder_generation: Option<Generation>,
|
||||
width: Option<Px>,
|
||||
color: Color,
|
||||
mask_bytes: usize,
|
||||
) -> bool {
|
||||
self.generation == generation
|
||||
&& self.mask_generation == mask_generation
|
||||
&& self.placeholder_generation == placeholder_generation
|
||||
&& self.width == width
|
||||
&& self.color == color
|
||||
&& self.mask_bytes == mask_bytes
|
||||
|
|
@ -118,6 +124,7 @@ where
|
|||
.then(|| CowString::from('\u{2022}'))
|
||||
.unwrap_or_default()
|
||||
.into_value(),
|
||||
placeholder: Value::default(),
|
||||
cache: None,
|
||||
blink_state: BlinkState::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`.
|
||||
///
|
||||
/// Only the first unicode grapheme will be used for the symbol. A warning
|
||||
|
|
@ -212,9 +226,14 @@ where
|
|||
return;
|
||||
}
|
||||
|
||||
// TODO remove a full grapheme
|
||||
let removed = value.as_string_mut().remove(cursor.offset - 1);
|
||||
self.selection.cursor.offset -= removed.len_utf8();
|
||||
if let Ok(Some(offset)) = GraphemeCursor::new(cursor.offset, value.as_str().len(), true)
|
||||
.prev_boundary(value.as_str(), 0)
|
||||
{
|
||||
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 generation = self.value.generation();
|
||||
let mask_generation = self.mask_symbol.generation();
|
||||
let placeholder_generation = self.placeholder.generation();
|
||||
let mut mask_bytes = self
|
||||
.mask_symbol
|
||||
.map(|sym| sym.graphemes(true).next().map_or(0, str::len));
|
||||
|
|
@ -544,9 +564,16 @@ where
|
|||
context.invalidate_when_changed(&self.value);
|
||||
match &mut self.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 bytes = text.len();
|
||||
|
||||
|
|
@ -580,16 +607,21 @@ where
|
|||
if let Some(width) = 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 {
|
||||
bytes,
|
||||
color,
|
||||
generation,
|
||||
mask_generation,
|
||||
placeholder_generation,
|
||||
mask_bytes,
|
||||
width,
|
||||
measured,
|
||||
placeholder,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -612,6 +644,7 @@ where
|
|||
let cache = self.cache.as_ref().expect("always initialized");
|
||||
CacheInfo {
|
||||
measured: &cache.measured,
|
||||
placeholder: &cache.placeholder,
|
||||
bytes: cache.bytes,
|
||||
masked: mask_bytes > 0,
|
||||
cursor,
|
||||
|
|
@ -877,6 +910,7 @@ where
|
|||
|
||||
struct CacheInfo<'a> {
|
||||
measured: &'a MeasuredText<Px>,
|
||||
placeholder: &'a MeasuredText<Px>,
|
||||
bytes: usize,
|
||||
masked: bool,
|
||||
cursor: Cursor,
|
||||
|
|
@ -1076,9 +1110,14 @@ where
|
|||
context.stroke_outline::<Lp>(outline_color, StrokeOptions::default());
|
||||
}
|
||||
|
||||
let text = if cache.bytes > 0 {
|
||||
cache.measured
|
||||
} else {
|
||||
cache.placeholder
|
||||
};
|
||||
context
|
||||
.gfx
|
||||
.draw_measured_text(cache.measured.translate_by(padding), TextOrigin::TopLeft);
|
||||
.draw_measured_text(text.translate_by(padding), TextOrigin::TopLeft);
|
||||
}
|
||||
|
||||
fn layout(
|
||||
|
|
@ -1092,7 +1131,12 @@ where
|
|||
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -409,9 +409,13 @@ impl Layout {
|
|||
) -> Size<UPx> {
|
||||
let (space_constraint, other_constraint) = self.orientation.split_size(available);
|
||||
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 =
|
||||
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);
|
||||
// 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
|
||||
|
|
@ -421,7 +425,7 @@ impl Layout {
|
|||
|
||||
// Measure the children that fit their content
|
||||
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 (measured, other) = self.orientation.split_size(measure(
|
||||
index,
|
||||
|
|
@ -430,7 +434,16 @@ impl Layout {
|
|||
!needs_final_layout,
|
||||
));
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -495,7 +508,9 @@ impl Layout {
|
|||
let mut offset = UPx::ZERO;
|
||||
for index in 0..self.children.len() {
|
||||
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 {
|
||||
self.orientation.split_size(measure(
|
||||
index,
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ impl WrapperWidget for Switcher {
|
|||
context.remove_child(&removed);
|
||||
}
|
||||
}
|
||||
context.invalidate_when_changed(&self.source);
|
||||
available_space
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ impl MakeWidget for Validated {
|
|||
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);
|
||||
|
|
@ -75,7 +74,6 @@ impl MakeWidget for Validated {
|
|||
// TODO these should be components
|
||||
.with(&TextSize, Lp::points(9))
|
||||
.with(&LineHeight, Lp::points(13))
|
||||
.collapse_vertically(collapse)
|
||||
.align_left(),
|
||||
)
|
||||
.into_rows(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue