From 41138bb6d7c76b52141902e522635b309dc4d031 Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Sun, 30 Jun 2024 11:22:28 -0400 Subject: [PATCH] Add tests --- cursive-core/src/builder.rs | 639 +---------------- cursive-core/src/builder/resolvable.rs | 951 +++++++++++++++++++++++++ cursive-core/src/style/color.rs | 39 +- cursive-core/src/style/gradient/mod.rs | 6 +- 4 files changed, 994 insertions(+), 641 deletions(-) create mode 100644 cursive-core/src/builder/resolvable.rs diff --git a/cursive-core/src/builder.rs b/cursive-core/src/builder.rs index aa3574e0..5bd77665 100644 --- a/cursive-core/src/builder.rs +++ b/cursive-core/src/builder.rs @@ -18,10 +18,14 @@ //! - A public part, always enabled. //! - An implementation module, conditionally compiled. #![cfg_attr(not(feature = "builder"), allow(unused))] + +mod resolvable; + +pub use self::resolvable::{NoConfig, Resolvable}; + use crate::views::BoxedView; use std::collections::{HashMap, HashSet}; -use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::any::Any; @@ -304,639 +308,6 @@ fn inspect_variables(config: &Config, on_var: &mut F) { _ => (), } } - -/// Trait for types that can be resolved from a context. -/// -/// They can be loaded from a config (yaml), or from a stored value (`Box`). -pub trait Resolvable { - /// Build from a config (a JSON value). - /// - /// The default implementation always fails. - fn from_config(config: &Config, _context: &Context) -> Result - where - Self: Sized, - { - Err(Error::CouldNotLoad { - expected_type: std::any::type_name::().to_string(), - config: config.clone(), - }) - } - - /// Build from an `Any` variable. - /// - /// Default implementation tries to downcast to `Self`. - /// - /// Override if you want to try downcasting to other types as well. - fn from_any(any: Box) -> Option - where - Self: Sized + Any, - { - any.downcast().ok().map(|b| *b) - } -} - -// Implement a trait for Fn(A, B), Fn(&A, B), Fn(A, &B), ... -// We do this by going down a tree: -// (D C B A) -// (C B A) to handle the case for 3 args -// ... -// <> [] (D C B A) to start actual work for 4 args -// [D] (C B A) | -// [&D] (C B A) | Here we branch and recurse -// [&mut D] (C B A) | -// ... -// [A B C D] () | -// ... | -// [A B &C D] () | Final implementations -// ... | -// [&mut A &mut B &mut C &mut D] () -macro_rules! impl_fn_from_config { - // Here is a graceful end for recursion. - ( - $trait:ident - ( ) - ) => { }; - ( - $trait:ident - < $($letters:ident $(: ?$unbound:ident)?)* > - [ $($args:ty)* ] - ( ) - ) => { - // The leaf node is the actual implementation - #[allow(coherence_leak_check)] - impl $trait for Arc Res + Send + Sync> {} - }; - ( - $trait:ident - < $($letters:ident $(: ?$unbound:ident)?)* > - [ $($args:ty)* ] - ( $head:ident $($leftover:ident)* ) - ) => { - // Here we just branch per ref type - impl_fn_from_config!( - $trait - < $head $($letters $(: ?$unbound)?)* > - [ $head $($args)* ] - ( $($leftover)* ) - ); - impl_fn_from_config!( - $trait - < $head: ?Sized $($letters $(: ?$unbound)?)* > - [ & $head $($args)* ] - ( $($leftover)* ) - ); - impl_fn_from_config!( - $trait - < $head: ?Sized $($letters $(: ?$unbound)?)* > - [ &mut $head $($args)* ] - ( $($leftover)* ) - ); - }; - ( - $trait:ident - ( $head:ident $($leftover:ident)* ) - ) => { - // First, branch out both the true implementation and the level below. - impl_fn_from_config!( - $trait - <> - [] - ( $head $($leftover)* ) - ); - impl_fn_from_config!( - $trait - ( $($leftover)* ) - ); - }; -} - -/// A wrapper around a value that cannot be parsed from config, but can still be stored/retrieved -/// in a context. -/// -/// This brings a `Resolvable` implementation that will always fail. -pub struct NoConfig(pub T); - -impl NoConfig { - /// Return the wrapped object. - pub fn into_inner(self) -> T { - self.0 - } -} - -impl From for NoConfig { - fn from(t: T) -> Self { - NoConfig(t) - } -} - -// Implement Resolvable for the wrapper, so we can resolve it. -impl Resolvable for NoConfig { - // We leave from_config as default (it'll always fail). - // As stated in the name, this cannot be loaded from a Config. - - // But when loading from a variable, accept an unwrapped value. - // - // So users can store a `T` and load it as `NoConfig`. - fn from_any(any: Box) -> Option - where - Self: Sized + Any, - { - // First try an actual NoConfig - any.downcast() - .map(|b| *b) - // Then, try a bare T - .or_else(|any| any.downcast::().map(|b| NoConfig(*b))) - .ok() - } -} - -// TODO: This could be solved with NoConfig instead. -// Implement Resolvable for all functions taking 4 or less arguments. -// (They will all fail to deserialize, but at least we can call resolve() on them) -// We could consider increasing that? It would probably increase compilation time, and clutter the -// Resolvable doc page. Maybe behind a feature if people really need it? -// (Ideally we wouldn't need it and we'd have a blanket implementation instead, but that may -// require specialization.) -impl_fn_from_config!(Resolvable (D C B A)); - -impl Resolvable for Option -where - T: Resolvable, -{ - fn from_config(config: &Config, context: &Context) -> Result { - if let Config::Null = config { - Ok(None) - } else { - Ok(Some(T::from_config(config, context)?)) - } - } - - fn from_any(any: Box) -> Option - where - Self: Sized + Any, - { - // First try the option, then try bare T. - any.downcast().map(|b| *b) - // Here we have a Result, _> - .unwrap_or_else(|any| T::from_any(any).map(|b| Some(b))) - } -} - -impl Resolvable for Config { - fn from_config(config: &Config, _context: &Context) -> Result { - Ok(config.clone()) - } -} - -impl Resolvable for Object { - fn from_config(config: &Config, _context: &Context) -> Result { - config - .as_object() - .ok_or_else(|| Error::invalid_config("Expected an object", config)) - .cloned() - } -} - -impl Resolvable for Box { - fn from_config(config: &Config, context: &Context) -> Result { - let boxed: BoxedView = context.build(config)?; - Ok(boxed.unwrap()) - } -} - -impl Resolvable for BoxedView { - fn from_config(config: &Config, context: &Context) -> Result { - context.build(config) - } -} - -impl Resolvable for crate::style::BaseColor { - fn from_config(config: &Config, _context: &Context) -> Result { - (|| Self::parse(config.as_str()?))().ok_or_else(|| Error::InvalidConfig { - message: "Invalid config for BaseColor".into(), - config: config.clone(), - }) - } -} - -impl Resolvable for crate::style::Palette { - fn from_config(config: &Config, context: &Context) -> Result { - let mut palette = Self::default(); - - let config = config - .as_object() - .ok_or_else(|| Error::invalid_config("Expected object", config))?; - - for (key, value) in config { - if let Ok(value) = context.resolve(value) { - palette.set_color(key, value); - } else if let Some(value) = value.as_object() { - // We don't currently support namespace themes here. - // ¯\_(ツ)_/¯ - log::warn!( - "Namespaces are not currently supported in configs. (When reading color for `{key}`: {value:?}.)" - ); - } - } - - Ok(palette) - } -} - -impl Resolvable for crate::style::BorderStyle { - fn from_config(config: &Config, context: &Context) -> Result { - let borders: String = context.resolve(config)?; - - Ok(Self::from(&borders)) - } -} - -impl Resolvable for crate::theme::Theme { - fn from_config(config: &Config, context: &Context) -> Result { - let mut theme = Self::default(); - - if let Some(shadow) = context.resolve(&config["shadow"])? { - theme.shadow = shadow; - } - - if let Some(borders) = context.resolve(&config["borders"])? { - theme.borders = borders; - } - - if let Some(palette) = context.resolve(&config["palette"])? { - theme.palette = palette; - } - - Ok(theme) - } -} - -// A bunch of `impl From` can easily implement Resolvable -impl Resolvable for Box -where - T: 'static + Resolvable, -{ - fn from_config(config: &Config, context: &Context) -> Result { - Ok(Box::new(T::from_config(config, context)?)) - } - - fn from_any(any: Box) -> Option { - // First try a Box - match any.downcast::().map(|b| *b) { - Ok(res) => Some(res), - // If it fails, try T::from_any (unboxed stored value) - Err(any) => T::from_any(any).map(Into::into), - } - } -} - -impl Resolvable for Arc -where - T: 'static + Resolvable, -{ - fn from_config(config: &Config, context: &Context) -> Result { - Ok(Arc::new(T::from_config(config, context)?)) - } - - fn from_any(any: Box) -> Option { - // First try a Arc - match any.downcast::().map(|b| *b) { - Ok(res) => Some(res), - Err(any) => T::from_any(any).map(Into::into), - } - } -} - -impl Resolvable for HashMap -where - T: 'static + Resolvable, -{ - fn from_config(config: &Config, context: &Context) -> Result { - let config = match config { - Config::Null => return Ok(HashMap::new()), - Config::Object(config) => config, - // Missing value get an empty vec - _ => return Err(Error::invalid_config("Expected array", config)), - }; - - config - .iter() - .map(|(k, v)| context.resolve(v).map(|v| (k.to_string(), v))) - .collect() - } -} - -impl Resolvable for Vec -where - T: 'static + Resolvable, -{ - fn from_config(config: &Config, context: &Context) -> Result { - let config = match config { - Config::Array(config) => config, - // Missing value get an empty vec - Config::Null => return Ok(Vec::new()), - _ => return Err(Error::invalid_config("Expected array", config)), - }; - - config.iter().map(|v| context.resolve(v)).collect() - } - - // TODO: Allow loading from `Vec>` and downcasting one by one? -} - -impl Resolvable for [T; N] -where - T: 'static + Resolvable + Clone, -{ - fn from_config(config: &Config, context: &Context) -> Result { - let vec = Vec::::from_config(config, context)?; - vec.try_into() - .map_err(|_| Error::invalid_config("Expected array of size {N}", config)) - } -} - -// ```yaml -// color: red -// color: -// dark: red -// color: -// rgb: [1, 2, 4] -// ``` -impl Resolvable for crate::style::Color { - fn from_config(config: &Config, context: &Context) -> Result { - Ok(match config { - Config::String(config) => Self::parse(config) - .ok_or_else(|| Error::invalid_config("Could not parse color", config))?, - Config::Object(config) => { - // Possibly keywords: - // - light - // - dark - // - rgb - let (key, value) = config - .iter() - .next() - .ok_or_else(|| Error::invalid_config("", config))?; - match key.as_str() { - "light" => Self::Light(context.resolve(value)?), - "dark" => Self::Dark(context.resolve(value)?), - "rgb" => { - let array: [u8; 3] = context.resolve(value)?; - Self::Rgb(array[0], array[1], array[2]) - } - _ => return Err(Error::invalid_config("Found unexpected key", config)), - } - } - Config::Array(_) => { - // Assume r, g, b - let array: [u8; 3] = context.resolve(config)?; - Self::Rgb(array[0], array[1], array[2]) - } - _ => return Err(Error::invalid_config("Found unsupported type", config)), - }) - } -} - -impl Resolvable for crate::style::PaletteColor { - fn from_config(config: &Config, context: &Context) -> Result { - let color: String = context.resolve(config)?; - - crate::style::PaletteColor::from_str(&color) - .map_err(|_| Error::invalid_config("Unrecognized palette color", config)) - } -} - -impl Resolvable for crate::style::ColorType { - fn from_config(config: &Config, context: &Context) -> Result { - if let Ok(color) = context.resolve(config) { - return Ok(Self::Color(color)); - } - - match config { - Config::String(config) => Self::from_str(config) - .map_err(|_| Error::invalid_config("Unrecognized color type", config)), - Config::Object(config) => { - // Try to load as a color? - let (key, value) = config - .iter() - .next() - .ok_or_else(|| Error::invalid_config("Found empty object", config))?; - Ok(match key.as_str() { - "palette" => Self::Palette(context.resolve(value)?), - "color" => Self::Color(context.resolve(value)?), - _ => { - return Err(Error::invalid_config( - format!("Found unrecognized key `{key}` in color type config"), - config, - )) - } - }) - } - _ => Err(Error::invalid_config("Expected string or object", config)), - } - } -} - -impl Resolvable for crate::style::ColorStyle { - fn from_config(config: &Config, context: &Context) -> Result { - if let Ok(color) = (|| -> Result<_, Error> { - let front = context.resolve(&config["front"])?; - let back = context.resolve(&config["back"])?; - - Ok(crate::style::ColorStyle { front, back }) - })() { - return Ok(color); - } - - unimplemented!() - } -} - -impl Resolvable for crate::view::Offset { - fn from_config(config: &Config, context: &Context) -> Result { - if let Some("center" | "Center") = config.as_str() { - return Ok(Self::Center); - } - - let config = config - .as_object() - .ok_or_else(|| Error::invalid_config("Expected `center` or an object.", config))?; - - let (key, value) = config - .iter() - .next() - .ok_or_else(|| Error::invalid_config("Expected non-empty object.", config))?; - - match key.as_str() { - "Absolute" | "absolute" => Ok(Self::Absolute(context.resolve(value)?)), - "Parent" | "parent" => Ok(Self::Parent(context.resolve(value)?)), - _ => Err(Error::invalid_config("Unexpected key `{key}`.", config)), - } - } -} - -// Literals don't need a context at all - -impl Resolvable for String { - fn from_config(config: &Config, _context: &Context) -> Result { - match config.as_str() { - Some(config) => Ok(config.into()), - None => Err(Error::invalid_config("Expected string type", config)), - } - } -} - -impl Resolvable for crate::utils::markup::StyledString { - fn from_config(config: &Config, context: &Context) -> Result - where - Self: Sized, - { - let text: String = context.resolve(config)?; - Ok(Self::plain(text)) - } - - fn from_any(any: Box) -> Option - where - Self: Sized + Any, - { - let any = match any.downcast::().map(|b| *b) { - Ok(res) => return Some(res), - Err(any) => any, - }; - - any.downcast::().map(|b| Self::plain(*b)).ok() - } -} - -impl Resolvable for bool { - fn from_config(config: &Config, _context: &Context) -> Result { - config - .as_bool() - .ok_or_else(|| Error::invalid_config("Expected bool type", config)) - } -} - -macro_rules! resolve_unsigned { - ($ty:ty) => { - impl Resolvable for $ty { - fn from_config(config: &Config, _context: &Context) -> Result { - config - .as_u64() - .and_then(|config| Self::try_from(config).ok()) - .ok_or_else(|| { - Error::invalid_config(format!("Expected unsigned <= {}", Self::MAX), config) - }) - } - } - }; -} -macro_rules! resolve_signed { - ($ty:ty) => { - impl Resolvable for $ty { - fn from_config(config: &Config, _context: &Context) -> Result { - config - .as_i64() - .and_then(|config| Self::try_from(config).ok()) - .ok_or_else(|| { - Error::invalid_config( - format!("Expected {} <= unsigned <= {}", Self::MIN, Self::MAX,), - config, - ) - }) - } - } - }; -} - -resolve_unsigned!(u8); -resolve_unsigned!(u16); -resolve_unsigned!(u32); -resolve_unsigned!(u64); -resolve_unsigned!(usize); - -resolve_signed!(i8); -resolve_signed!(i16); -resolve_signed!(i32); -resolve_signed!(i64); -resolve_signed!(isize); - -impl Resolvable for crate::XY { - fn from_config(config: &Config, context: &Context) -> Result { - Ok(match config { - Config::Array(config) if config.len() == 2 => { - let x = context.resolve(&config[0])?; - let y = context.resolve(&config[1])?; - crate::XY::new(x, y) - } - Config::Object(config) => { - let x = context.resolve(&config["x"])?; - let y = context.resolve(&config["y"])?; - crate::XY::new(x, y) - } - // That one would require specialization? - // Config::String(config) if config == "zero" => crate::Vec2::zero(), - config => { - return Err(Error::invalid_config( - "Expected Array of length 2, object, or 'zero'.", - config, - )) - } - }) - } -} - -impl Resolvable for crate::direction::Orientation { - fn from_config(config: &Config, context: &Context) -> Result { - let value: String = context.resolve(config)?; - Ok(match value.as_str() { - "vertical" | "Vertical" => Self::Vertical, - "horizontal" | "Horizontal" => Self::Horizontal, - _ => { - return Err(Error::invalid_config( - "Unrecognized orientation. Should be horizontal or vertical.", - config, - )) - } - }) - } -} - -impl Resolvable for crate::view::Margins { - fn from_config(config: &Config, context: &Context) -> Result { - Ok(match config { - Config::Object(config) => Self::lrtb( - context.resolve(&config["left"])?, - context.resolve(&config["right"])?, - context.resolve(&config["top"])?, - context.resolve(&config["bottom"])?, - ), - Config::Number(_) => { - let n = context.resolve(config)?; - Self::lrtb(n, n, n, n) - } - _ => return Err(Error::invalid_config("Expected object or number", config)), - }) - } -} - -impl Resolvable for crate::align::HAlign { - fn from_config(config: &Config, _context: &Context) -> Result { - // TODO: also resolve single-value configs like strings. - // Also when resolving a variable with the wrong type, fallback on loading the type with - // the variable name. - Ok(match config.as_str() { - Some(config) if config == "Left" || config == "left" => Self::Left, - Some(config) if config == "Center" || config == "center" => Self::Center, - Some(config) if config == "Right" || config == "right" => Self::Right, - _ => { - return Err(Error::invalid_config( - "Expected left, center or right", - config, - )) - } - }) - } -} - new_default!(Context); impl Context { diff --git a/cursive-core/src/builder/resolvable.rs b/cursive-core/src/builder/resolvable.rs new file mode 100644 index 00000000..e4d3e997 --- /dev/null +++ b/cursive-core/src/builder/resolvable.rs @@ -0,0 +1,951 @@ +use super::{Config, Context, Error, Object}; +use crate::views::BoxedView; + +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Arc; + +use std::any::Any; + +/// Trait for types that can be resolved from a context. +/// +/// They can be loaded from a config (yaml), or from a stored value (`Box`). +pub trait Resolvable { + /// Build from a config (a JSON value). + /// + /// The default implementation always fails. + fn from_config(config: &Config, _context: &Context) -> Result + where + Self: Sized, + { + Err(Error::CouldNotLoad { + expected_type: std::any::type_name::().to_string(), + config: config.clone(), + }) + } + + /// Build from an `Any` variable. + /// + /// Default implementation tries to downcast to `Self`. + /// + /// Override if you want to try downcasting to other types as well. + fn from_any(any: Box) -> Option + where + Self: Sized + Any, + { + any.downcast().ok().map(|b| *b) + } +} + +// Implement a trait for Fn(A, B), Fn(&A, B), Fn(A, &B), ... +// We do this by going down a tree: +// (D C B A) +// (C B A) to handle the case for 3 args +// ... +// <> [] (D C B A) to start actual work for 4 args +// [D] (C B A) | +// [&D] (C B A) | Here we branch and recurse +// [&mut D] (C B A) | +// ... +// [A B C D] () | +// ... | +// [A B &C D] () | Final implementations +// ... | +// [&mut A &mut B &mut C &mut D] () +macro_rules! impl_fn_from_config { + // Here is a graceful end for recursion. + ( + $trait:ident + ( ) + ) => { }; + ( + $trait:ident + < $($letters:ident $(: ?$unbound:ident)?)* > + [ $($args:ty)* ] + ( ) + ) => { + // The leaf node is the actual implementation + #[allow(coherence_leak_check)] + impl $trait for Arc Res + Send + Sync> {} + }; + ( + $trait:ident + < $($letters:ident $(: ?$unbound:ident)?)* > + [ $($args:ty)* ] + ( $head:ident $($leftover:ident)* ) + ) => { + // Here we just branch per ref type + impl_fn_from_config!( + $trait + < $head $($letters $(: ?$unbound)?)* > + [ $head $($args)* ] + ( $($leftover)* ) + ); + impl_fn_from_config!( + $trait + < $head: ?Sized $($letters $(: ?$unbound)?)* > + [ & $head $($args)* ] + ( $($leftover)* ) + ); + impl_fn_from_config!( + $trait + < $head: ?Sized $($letters $(: ?$unbound)?)* > + [ &mut $head $($args)* ] + ( $($leftover)* ) + ); + }; + ( + $trait:ident + ( $head:ident $($leftover:ident)* ) + ) => { + // First, branch out both the true implementation and the level below. + impl_fn_from_config!( + $trait + <> + [] + ( $head $($leftover)* ) + ); + impl_fn_from_config!( + $trait + ( $($leftover)* ) + ); + }; +} + +/// A wrapper around a value that cannot be parsed from config, but can still be stored/retrieved +/// in a context. +/// +/// This brings a `Resolvable` implementation that will always fail. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct NoConfig(pub T); + +impl NoConfig { + /// Return the wrapped object. + pub fn into_inner(self) -> T { + self.0 + } +} + +impl From for NoConfig { + fn from(t: T) -> Self { + NoConfig(t) + } +} + +// Implement Resolvable for the wrapper, so we can resolve it. +impl Resolvable for NoConfig { + // We leave from_config as default (it'll always fail). + // As stated in the name, this cannot be loaded from a Config. + + // But when loading from a variable, accept an unwrapped value. + // + // So users can store a `T` and load it as `NoConfig`, without having to implement + // `Resolvable` for `T`. + fn from_any(any: Box) -> Option + where + Self: Sized + Any, + { + // First try an actual NoConfig + any.downcast() + .map(|b| *b) + // Then, try a bare T + .or_else(|any| any.downcast::().map(|b| NoConfig(*b))) + .ok() + } +} + +impl Resolvable for Option +where + T: Resolvable, +{ + fn from_config(config: &Config, context: &Context) -> Result { + if let Config::Null = config { + Ok(None) + } else { + Ok(Some(T::from_config(config, context)?)) + } + } + + fn from_any(any: Box) -> Option + where + Self: Sized + Any, + { + // First try the option, then try bare T. + any.downcast::() + .map(|b| *b) + .or_else(|any| T::from_any(any).map(|b| Some(b)).ok_or(())) + .ok() + } +} + +impl Resolvable for Config { + fn from_config(config: &Config, _context: &Context) -> Result { + Ok(config.clone()) + } +} + +impl Resolvable for Object { + fn from_config(config: &Config, _context: &Context) -> Result { + config + .as_object() + .ok_or_else(|| Error::invalid_config("Expected an object", config)) + .cloned() + } +} + +impl Resolvable for Box { + fn from_config(config: &Config, context: &Context) -> Result { + let boxed: BoxedView = context.build(config)?; + Ok(boxed.unwrap()) + } +} + +impl Resolvable for BoxedView { + fn from_config(config: &Config, context: &Context) -> Result { + context.build(config) + } +} + +impl Resolvable for crate::style::BaseColor { + fn from_config(config: &Config, _context: &Context) -> Result { + (|| Self::parse(config.as_str()?))().ok_or_else(|| Error::InvalidConfig { + message: "Invalid config for BaseColor".into(), + config: config.clone(), + }) + } +} + +impl Resolvable for crate::style::Palette { + fn from_config(config: &Config, context: &Context) -> Result { + let mut palette = Self::default(); + + let config = config + .as_object() + .ok_or_else(|| Error::invalid_config("Expected object", config))?; + + for (key, value) in config { + if let Ok(value) = context.resolve(value) { + palette.set_color(key, value); + } else if let Some(value) = value.as_object() { + // We don't currently support namespace themes here. + // ¯\_(ツ)_/¯ + log::warn!( + "Namespaces are not currently supported in configs. (When reading color for `{key}`: {value:?}.)" + ); + } + } + + Ok(palette) + } +} + +impl Resolvable for crate::style::BorderStyle { + fn from_config(config: &Config, context: &Context) -> Result { + let borders: String = context.resolve(config)?; + + Ok(Self::from(&borders)) + } +} + +impl Resolvable for crate::theme::Theme { + fn from_config(config: &Config, context: &Context) -> Result { + let mut theme = Self::default(); + + if let Some(shadow) = context.resolve(&config["shadow"])? { + theme.shadow = shadow; + } + + if let Some(borders) = context.resolve(&config["borders"])? { + theme.borders = borders; + } + + if let Some(palette) = context.resolve(&config["palette"])? { + theme.palette = palette; + } + + Ok(theme) + } +} + +// A bunch of `impl From` can easily implement Resolvable +impl Resolvable for Box +where + T: 'static + Resolvable, +{ + fn from_config(config: &Config, context: &Context) -> Result { + Ok(Box::new(T::from_config(config, context)?)) + } + + fn from_any(any: Box) -> Option { + // First try a Box + match any.downcast::().map(|b| *b) { + Ok(res) => Some(res), + // If it fails, try T::from_any (unboxed stored value) + Err(any) => T::from_any(any).map(Into::into), + } + } +} + +impl Resolvable for Arc +where + T: 'static + Resolvable, +{ + fn from_config(config: &Config, context: &Context) -> Result { + Ok(Arc::new(T::from_config(config, context)?)) + } + + fn from_any(any: Box) -> Option { + // First try a Arc + match any.downcast::().map(|b| *b) { + Ok(res) => Some(res), + Err(any) => T::from_any(any).map(Into::into), + } + } +} + +impl Resolvable for HashMap +where + T: 'static + Resolvable, +{ + fn from_config(config: &Config, context: &Context) -> Result { + let config = match config { + Config::Null => return Ok(HashMap::new()), + Config::Object(config) => config, + // Missing value get an empty vec + _ => return Err(Error::invalid_config("Expected object", config)), + }; + + config + .iter() + .map(|(k, v)| context.resolve(v).map(|v| (k.to_string(), v))) + .collect() + } +} + +impl Resolvable for Vec +where + T: 'static + Resolvable, +{ + fn from_config(config: &Config, context: &Context) -> Result { + let config = match config { + Config::Array(config) => config, + // Missing value get an empty vec + Config::Null => return Ok(Vec::new()), + _ => return Err(Error::invalid_config("Expected array", config)), + }; + + config.iter().map(|v| context.resolve(v)).collect() + } + + // TODO: Allow loading from `Vec>` and downcasting one by one? +} + +impl Resolvable for [T; N] +where + T: 'static + Resolvable + Clone, +{ + fn from_any(any: Box) -> Option + where + Self: Sized + Any, + { + // Allow storing a `Vec` with the correct size + any.downcast() + .map(|b| *b) + .or_else(|any| { + any.downcast::>() + .ok() + .and_then(|vec| (*vec).try_into().ok()) + .ok_or(()) + }) + .ok() + } + + fn from_config(config: &Config, context: &Context) -> Result { + let vec = Vec::::from_config(config, context)?; + vec.try_into() + .map_err(|_| Error::invalid_config("Expected array of size {N}", config)) + } +} + +impl Resolvable for crate::style::Rgb { + fn from_any(any: Box) -> Option { + // Accept both Rgb and Rgb as stored values. + any.downcast() + .map(|b| *b) + .or_else(|any| { + any.downcast::>() + .map(|rgb| rgb.as_f32()) + }) + .ok() + } + + fn from_config(config: &Config, context: &Context) -> Result + where + Self: Sized, + { + // Try as a hex string? + if let Ok(rgb) = context.resolve::(config) { + if let Ok(rgb) = rgb.parse::>() { + return Ok(rgb.as_f32()); + } + } + + // Allow storing a list of f32 or a list of u8. + if let Ok(rgb) = context.resolve::<[u8; 3]>(config) { + // Try u8 first. If it's all u8, trying as f32 would also work. + return Ok(crate::style::Rgb::::from(rgb).as_f32()); + } + + if let Ok(rgb) = context.resolve::<[f32; 3]>(config) { + return Ok(Self::from(rgb)); + } + + // TODO: Here too, try as u8 first, then again as f32. + if let Some(rgb) = config.as_object().and_then(|config| { + let r = config.get("r").or_else(|| config.get("R"))?; + let g = config.get("g").or_else(|| config.get("G"))?; + let b = config.get("b").or_else(|| config.get("B"))?; + + let r = context.resolve(r).ok()?; + let g = context.resolve(g).ok()?; + let b = context.resolve(b).ok()?; + + Some(Self { r, g, b }) + }) { + return Ok(rgb); + } + + Err(Error::invalid_config( + "Could not parse as a RGB color.", + config, + )) + } +} + +impl Resolvable for crate::style::Rgb { + fn from_any(any: Box) -> Option + where + Self: Sized + Any, + { + // Accept both Rgb and Rgb as stored values. + any.downcast() + .map(|b| *b) + .or_else(|any| { + any.downcast::>() + .map(|rgb| rgb.as_u8()) + }) + .ok() + } + + fn from_config(config: &Config, context: &Context) -> Result + where + Self: Sized, + { + // Try as a hex string? + if let Ok(rgb) = context.resolve::(config) { + if let Ok(rgb) = rgb.parse::>() { + return Ok(rgb); + } + } + + // Allow storing a list of f32 or a list of u8. + if let Ok(rgb) = context.resolve::<[u8; 3]>(config) { + // Try u8 first. If it's all u8, trying as f32 would also work. + return Ok(Self::from(rgb)); + } + + // TODO: Here too, try as u8 first, then again as f32. + if let Some(rgb) = config.as_object().and_then(|config| { + let r = config.get("r").or_else(|| config.get("R"))?; + let g = config.get("g").or_else(|| config.get("G"))?; + let b = config.get("b").or_else(|| config.get("B"))?; + + let r = context.resolve(r).ok()?; + let g = context.resolve(g).ok()?; + let b = context.resolve(b).ok()?; + + Some(Self { r, g, b }) + }) { + return Ok(rgb); + } + + let rgb: [f32; 3] = context.resolve(config)?; + Ok(crate::style::Rgb::::from(rgb).as_u8()) + } +} + +// ```yaml +// color: red +// color: +// dark: red +// color: +// rgb: [1, 2, 4] +// ``` +impl Resolvable for crate::style::Color { + fn from_config(config: &Config, context: &Context) -> Result { + Ok(match config { + Config::String(config) => Self::parse(config) + .ok_or_else(|| Error::invalid_config("Could not parse color", config))?, + Config::Object(config) => { + // Possibly keywords: + // - light + // - dark + // - rgb + let (key, value) = config + .iter() + .next() + .ok_or_else(|| Error::invalid_config("", config))?; + match key.as_str() { + "light" => Self::Light(context.resolve(value)?), + "dark" => Self::Dark(context.resolve(value)?), + "rgb" => { + let array: [u8; 3] = context.resolve(value)?; + Self::Rgb(array[0], array[1], array[2]) + } + _ => return Err(Error::invalid_config("Found unexpected key", config)), + } + } + Config::Array(_) => { + // Assume r, g, b + let array: [u8; 3] = context.resolve(config)?; + Self::Rgb(array[0], array[1], array[2]) + } + _ => return Err(Error::invalid_config("Found unsupported type", config)), + }) + } +} + +impl Resolvable for crate::style::PaletteColor { + fn from_config(config: &Config, context: &Context) -> Result { + let color: String = context.resolve(config)?; + + crate::style::PaletteColor::from_str(&color) + .map_err(|_| Error::invalid_config("Unrecognized palette color", config)) + } +} + +impl Resolvable for crate::style::ColorType { + fn from_config(config: &Config, context: &Context) -> Result { + if let Ok(color) = context.resolve(config) { + return Ok(Self::Color(color)); + } + + match config { + Config::String(config) => Self::from_str(config) + .map_err(|_| Error::invalid_config("Unrecognized color type", config)), + Config::Object(config) => { + // Try to load as a color? + let (key, value) = config + .iter() + .next() + .ok_or_else(|| Error::invalid_config("Found empty object", config))?; + Ok(match key.as_str() { + "palette" => Self::Palette(context.resolve(value)?), + "color" => Self::Color(context.resolve(value)?), + _ => { + return Err(Error::invalid_config( + format!("Found unrecognized key `{key}` in color type config"), + config, + )) + } + }) + } + _ => Err(Error::invalid_config("Expected string or object", config)), + } + } +} + +impl Resolvable for crate::style::ColorStyle { + fn from_config(config: &Config, context: &Context) -> Result { + if let Ok(color) = (|| -> Result<_, Error> { + let front = context.resolve(&config["front"])?; + let back = context.resolve(&config["back"])?; + + Ok(crate::style::ColorStyle { front, back }) + })() { + return Ok(color); + } + + unimplemented!() + } +} + +impl Resolvable for crate::view::Offset { + fn from_config(config: &Config, context: &Context) -> Result { + if let Some("center" | "Center") = config.as_str() { + return Ok(Self::Center); + } + + let config = config + .as_object() + .ok_or_else(|| Error::invalid_config("Expected `center` or an object.", config))?; + + let (key, value) = config + .iter() + .next() + .ok_or_else(|| Error::invalid_config("Expected non-empty object.", config))?; + + match key.as_str() { + "Absolute" | "absolute" => Ok(Self::Absolute(context.resolve(value)?)), + "Parent" | "parent" => Ok(Self::Parent(context.resolve(value)?)), + _ => Err(Error::invalid_config("Unexpected key `{key}`.", config)), + } + } +} + +impl Resolvable for String { + fn from_config(config: &Config, _context: &Context) -> Result { + match config.as_str() { + Some(config) => Ok(config.into()), + None => Err(Error::invalid_config("Expected string type", config)), + } + } +} + +impl Resolvable for crate::utils::markup::StyledString { + fn from_config(config: &Config, context: &Context) -> Result + where + Self: Sized, + { + let text: String = context.resolve(config)?; + Ok(Self::plain(text)) + } + + fn from_any(any: Box) -> Option + where + Self: Sized + Any, + { + let any = match any.downcast::().map(|b| *b) { + Ok(res) => return Some(res), + Err(any) => any, + }; + + any.downcast::().map(|b| Self::plain(*b)).ok() + } +} + +impl Resolvable for bool { + fn from_config(config: &Config, _context: &Context) -> Result { + config + .as_bool() + .ok_or_else(|| Error::invalid_config("Expected bool type", config)) + } +} + +macro_rules! resolve_float { + ($ty:ty) => { + impl Resolvable for $ty { + fn from_any(any: Box) -> Option + { + // Accept both f32 and f64, just cast between them. + any.downcast::() + .map(|b| *b as Self) + .or_else(|any| { + any.downcast::() + .map(|b| *b as Self) + }) + .ok() + } + + fn from_config(config: &Config, _context: &Context) -> Result { + config + .as_f64() // This already handles converting from integers + .map(|config| config as Self) + .ok_or_else(|| Error::invalid_config(format!("Expected float value"), config)) + } + } + }; +} + +macro_rules! resolve_unsigned { + ($ty:ty) => { + impl Resolvable for $ty { + fn from_any(any: Box) -> Option { + // Accept any signed or unsigned integer, as long as it fits. + any.downcast::() + .map(|b| (*b).try_into().ok()) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .ok()? + } + + fn from_config(config: &Config, _context: &Context) -> Result { + config + .as_u64() + .and_then(|config| Self::try_from(config).ok()) + .ok_or_else(|| { + Error::invalid_config(format!("Expected unsigned <= {}", Self::MAX), config) + }) + } + } + }; +} + +macro_rules! resolve_signed { + ($ty:ty) => { + impl Resolvable for $ty { + fn from_any(any: Box) -> Option { + // Accept any signed or unsigned integer, as long as it fits. + any.downcast::() + .map(|b| (*b).try_into().ok()) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .or_else(|any| any.downcast::().map(|b| (*b).try_into().ok())) + .ok()? + } + + fn from_config(config: &Config, _context: &Context) -> Result { + config + .as_i64() + .and_then(|config| Self::try_from(config).ok()) + .ok_or_else(|| { + Error::invalid_config( + format!("Expected {} <= unsigned <= {}", Self::MIN, Self::MAX,), + config, + ) + }) + } + } + }; +} + +resolve_float!(f32); +resolve_float!(f64); + +resolve_unsigned!(u8); +resolve_unsigned!(u16); +resolve_unsigned!(u32); +resolve_unsigned!(u64); +resolve_unsigned!(u128); +resolve_unsigned!(usize); + +resolve_signed!(i8); +resolve_signed!(i16); +resolve_signed!(i32); +resolve_signed!(i64); +resolve_signed!(i128); +resolve_signed!(isize); + +impl Resolvable for crate::XY { + fn from_config(config: &Config, context: &Context) -> Result { + Ok(match config { + Config::Array(config) if config.len() == 2 => { + let x = context.resolve(&config[0])?; + let y = context.resolve(&config[1])?; + crate::XY::new(x, y) + } + Config::Object(config) => { + let x = context.resolve(&config["x"])?; + let y = context.resolve(&config["y"])?; + crate::XY::new(x, y) + } + // That one would require specialization? + // Config::String(config) if config == "zero" => crate::Vec2::zero(), + config => { + return Err(Error::invalid_config( + "Expected Array of length 2, object, or 'zero'.", + config, + )) + } + }) + } +} + +impl Resolvable for crate::direction::Orientation { + fn from_config(config: &Config, context: &Context) -> Result { + let value: String = context.resolve(config)?; + Ok(match value.as_str() { + "vertical" | "Vertical" => Self::Vertical, + "horizontal" | "Horizontal" => Self::Horizontal, + _ => { + return Err(Error::invalid_config( + "Unrecognized orientation. Should be horizontal or vertical.", + config, + )) + } + }) + } +} + +impl Resolvable for crate::view::Margins { + fn from_config(config: &Config, context: &Context) -> Result { + Ok(match config { + Config::Object(config) => Self::lrtb( + context.resolve(&config["left"])?, + context.resolve(&config["right"])?, + context.resolve(&config["top"])?, + context.resolve(&config["bottom"])?, + ), + Config::Number(_) => { + let n = context.resolve(config)?; + Self::lrtb(n, n, n, n) + } + _ => return Err(Error::invalid_config("Expected object or number", config)), + }) + } +} + +impl Resolvable for crate::align::HAlign { + fn from_config(config: &Config, _context: &Context) -> Result { + // TODO: also resolve single-value configs like strings. + // Also when resolving a variable with the wrong type, fallback on loading the type with + // the variable name. + Ok(match config.as_str() { + Some(config) if config == "Left" || config == "left" => Self::Left, + Some(config) if config == "Center" || config == "center" => Self::Center, + Some(config) if config == "Right" || config == "right" => Self::Right, + _ => { + return Err(Error::invalid_config( + "Expected left, center or right", + config, + )) + } + }) + } +} + +// TODO: This could be solved with NoConfig instead. +// Implement Resolvable for all functions taking 4 or less arguments. +// (They will all fail to deserialize, but at least we can call resolve() on them) +// We could consider increasing that? It would probably increase compilation time, and clutter the +// Resolvable doc page. Maybe behind a feature if people really need it? +// (Ideally we wouldn't need it and we'd have a blanket implementation instead, but that may +// require specialization.) +impl_fn_from_config!(Resolvable (D C B A)); + +#[cfg(test)] +mod tests { + use crate::{ + builder::{Config, Context}, + utils::markup::StyledString, + }; + + use super::Resolvable; + + fn check_resolves(value: T, result: R) + where + T: Clone + Send + Sync + 'static, + R: Resolvable + PartialEq + std::fmt::Debug + 'static, + { + let mut context = Context::new(); + context.store("foo", value); + let config = Config::String("$foo".into()); + assert_eq!(result, context.resolve::(&config).unwrap()); + } + + #[test] + fn test_integers() { + fn check_integer_types(value: T) + where + T: Clone + Send + Sync + 'static, + { + check_resolves(value.clone(), 1usize); + check_resolves(value.clone(), 1u8); + check_resolves(value.clone(), 1u16); + check_resolves(value.clone(), 1u32); + check_resolves(value.clone(), 1u64); + check_resolves(value.clone(), 1u128); + check_resolves(value.clone(), 1isize); + check_resolves(value.clone(), 1i8); + check_resolves(value.clone(), 1i16); + check_resolves(value.clone(), 1i32); + check_resolves(value.clone(), 1i64); + check_resolves(value.clone(), 1i128); + } + + check_integer_types(1usize); + check_integer_types(1u8); + check_integer_types(1u16); + check_integer_types(1u32); + check_integer_types(1u64); + check_integer_types(1u128); + check_integer_types(1isize); + check_integer_types(1i8); + check_integer_types(1i16); + check_integer_types(1i32); + check_integer_types(1i64); + check_integer_types(1i128); + } + + #[test] + fn test_floats() { + check_resolves(1.0f32, 1.0f32); + check_resolves(1.0f32, 1.0f64); + check_resolves(1.0f64, 1.0f32); + check_resolves(1.0f64, 1.0f64); + } + + #[test] + fn test_vec() { + // Vec to Vec + check_resolves(vec![1u32, 2, 3], vec![1u32, 2, 3]); + // Vec to Array + check_resolves(vec![1u32, 2, 3], [1u32, 2, 3]); + // Array to Array + check_resolves([1u32, 2, 3], [1u32, 2, 3]); + } + + #[test] + fn test_option() { + check_resolves(Some(42u32), Some(42u32)); + check_resolves(42u32, Some(42u32)); + } + + #[test] + fn test_box() { + check_resolves(Box::new(42u32), Box::new(42u32)); + check_resolves(42u32, Box::new(42u32)); + } + + #[test] + fn test_arc() { + use std::sync::Arc; + check_resolves(Arc::new(42u32), Arc::new(42u32)); + check_resolves(42u32, Arc::new(42u32)); + } + + #[test] + fn test_rgb() { + use crate::style::Rgb; + check_resolves(Rgb::new(0u8, 0u8, 255u8), Rgb::new(0u8, 0u8, 255u8)); + check_resolves(Rgb::new(0f32, 0f32, 1f32), Rgb::new(0u8, 0u8, 255u8)); + check_resolves(Rgb::new(0u8, 0u8, 255u8), Rgb::new(0f32, 0f32, 1f32)); + check_resolves(Rgb::new(0f32, 0f32, 1f32), Rgb::new(0f32, 0f32, 1f32)); + } + + #[test] + fn test_styled_string() { + check_resolves(String::from("foo"), StyledString::plain("foo")); + } + + #[test] + fn test_no_config() { + use super::NoConfig; + + // Test how NoConfig lets you store and resolve types that are not `Resolvable`. + + #[derive(Clone, PartialEq, Eq, Debug)] + struct Foo(i32); + + check_resolves(Foo(42), NoConfig(Foo(42))); + } +} diff --git a/cursive-core/src/style/color.rs b/cursive-core/src/style/color.rs index 82d3efd9..13936e83 100644 --- a/cursive-core/src/style/color.rs +++ b/cursive-core/src/style/color.rs @@ -251,6 +251,35 @@ impl Rgb { } } +impl FromStr for Rgb { + type Err = super::NoSuchColor; + + fn from_str(s: &str) -> Result { + match s { + "red" | "Red" => Ok(Self::red()), + "green" | "Green" => Ok(Self::green()), + "blue" | "Blue" => Ok(Self::blue()), + "yellow" | "Yellow" => Ok(Self::yellow()), + "magenta" | "Magenta" => Ok(Self::magenta()), + "cyan" | "Cyan" => Ok(Self::cyan()), + "white" | "White" => Ok(Self::white()), + "black" | "Black" => Ok(Self::black()), + s => { + // Remove `#` or `0x` prefix + let s = s + .strip_prefix('#') + .or_else(|| s.strip_prefix("0x")) + .unwrap_or(s); + if let Some(rgb) = parse_hex(s) { + Ok(rgb) + } else { + Err(super::NoSuchColor) + } + } + } + } +} + impl From> for Color { fn from(rgb: Rgb) -> Self { rgb.as_color() @@ -391,11 +420,11 @@ impl FromStr for Color { /// Optionally prefixed with `#` or `0x`. fn parse_hex_color(value: &str) -> Option { if let Some(value) = value.strip_prefix('#') { - parse_hex(value) + parse_hex(value).map(Color::from) } else if let Some(value) = value.strip_prefix("0x") { - parse_hex(value) + parse_hex(value).map(Color::from) } else if value.len() == 6 { - parse_hex(value) + parse_hex(value).map(Color::from) } else if value.len() == 3 { // RGB values between 0 and 5 maybe? // Like 050 for green @@ -413,7 +442,7 @@ fn parse_hex_color(value: &str) -> Option { } /// This parses a purely hex string (either rrggbb or rgb) into a color. -fn parse_hex(value: &str) -> Option { +fn parse_hex(value: &str) -> Option> { // Compute per-color length, and amplitude let (l, multiplier) = match value.len() { 6 => (2, 1), @@ -424,7 +453,7 @@ fn parse_hex(value: &str) -> Option { let g = load_hex(&value[l..2 * l]) * multiplier; let b = load_hex(&value[2 * l..3 * l]) * multiplier; - Some(Color::Rgb(r as u8, g as u8, b as u8)) + Some(Rgb::new(r as u8, g as u8, b as u8)) } /// Loads a hexadecimal code diff --git a/cursive-core/src/style/gradient/mod.rs b/cursive-core/src/style/gradient/mod.rs index bdb8deba..969253c3 100644 --- a/cursive-core/src/style/gradient/mod.rs +++ b/cursive-core/src/style/gradient/mod.rs @@ -156,7 +156,9 @@ impl Interpolator for Radial { // TODO: cache this for the same value of `size`? // (Define a type that combines the gradient and the size, to be re-used for a draw cycle?) let to_corner = self.center.map(|x| 0.5f32 + (x - 0.5f32).abs()) * size_f32; - let max_distance = (to_corner.map(|x| x as isize).sq_norm() as f32).sqrt().max(1.0); + let max_distance = (to_corner.map(|x| x as isize).sq_norm() as f32) + .sqrt() + .max(1.0); let center = (self.center * size_f32).map(|x| x as isize); @@ -242,7 +244,7 @@ pub struct Bilinear { impl Interpolator for Bilinear { fn interpolate(&self, pos: Vec2, size: Vec2) -> Rgb { - if !Vec2::new(2,2).fits_in(size) { + if !Vec2::new(2, 2).fits_in(size) { // Size=0 => doesn't matter // Size=1 => ??? first value? return self.top_left;