Add custom values to Palette

This updates `Palette` to add a tree of custom values.
Branches from this tree can be copied back to the root with
Palette::merge. This can be used to group related values together.
This commit is contained in:
Alexandre Bury 2018-06-10 23:24:58 -07:00
parent d31903cba9
commit 017e100b1b
4 changed files with 186 additions and 79 deletions

View File

@ -92,7 +92,7 @@ impl Cursive {
pub fn new(backend: Box<backend::Backend>) -> Self {
let theme = theme::load_default();
// theme.activate(&mut backend);
// let theme = theme::load_theme("assets/style.toml").unwrap();
// let theme = theme::load_toml("assets/style.toml").unwrap();
let (tx, rx) = mpsc::channel();
@ -270,8 +270,8 @@ impl Cursive {
/// Loads a theme from the given string content.
///
/// Content must be valid toml.
pub fn load_theme(&mut self, content: &str) -> Result<(), theme::Error> {
self.set_theme(try!(theme::load_theme(content)));
pub fn load_toml(&mut self, content: &str) -> Result<(), theme::Error> {
self.set_theme(try!(theme::load_toml(content)));
Ok(())
}

View File

@ -114,6 +114,13 @@ impl Color {
}
}
/// Parse a string into a color.
///
/// Examples:
/// * `"red"` becomes `Color::Dark(BaseColor::Red)`
/// * `"light green"` becomes `Color::Light(BaseColor::Green)`
/// * `"default"` becomes `Color::TerminalDefault`
/// * `"#123456"` becomes `Color::Rgb(0x12, 0x34, 0x56)`
pub(crate) fn parse(value: &str) -> Option<Self> {
Some(match value {
"black" => Color::Dark(BaseColor::Black),
@ -149,11 +156,15 @@ impl Color {
let r = load_hex(&value[0..l]) * multiplier;
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))
} else if value.len() == 3 {
// RGB values between 0 and 5 maybe?
// Like 050 for green
let rgb: Vec<_> =
value.chars().map(|c| c as i16 - '0' as i16).collect();
assert_eq!(rgb.len(), 3);
if rgb.iter().all(|&i| i >= 0 && i < 6) {
Some(Color::RgbLowRes(
rgb[0] as u8,

View File

@ -169,7 +169,7 @@ pub use self::color::{BaseColor, Color};
pub use self::color_pair::ColorPair;
pub use self::color_style::{ColorStyle, ColorType};
pub use self::effect::Effect;
pub use self::palette::{default_palette, Palette, PaletteColor};
pub use self::palette::{Palette, PaletteColor};
pub use self::style::Style;
use std::fs::File;
use std::io;
@ -193,13 +193,13 @@ impl Default for Theme {
Theme {
shadow: true,
borders: BorderStyle::Simple,
palette: default_palette(),
palette: Palette::default(),
}
}
}
impl Theme {
fn load(&mut self, table: &toml::value::Table) {
fn load_toml(&mut self, table: &toml::value::Table) {
if let Some(&toml::Value::Boolean(shadow)) = table.get("shadow") {
self.shadow = shadow;
}
@ -209,7 +209,7 @@ impl Theme {
}
if let Some(&toml::Value::Table(ref table)) = table.get("colors") {
palette::load_table(&mut self.palette, table);
palette::load_toml(&mut self.palette, table);
}
}
}
@ -244,15 +244,15 @@ pub fn load_theme_file<P: AsRef<Path>>(filename: P) -> Result<Theme, Error> {
content
};
load_theme(&content)
load_toml(&content)
}
/// Loads a theme string and sets it as active.
pub fn load_theme(content: &str) -> Result<Theme, Error> {
pub fn load_toml(content: &str) -> Result<Theme, Error> {
let table = toml::de::from_str(content)?;
let mut theme = Theme::default();
theme.load(&table);
theme.load_toml(&table);
Ok(theme)
}

View File

@ -2,6 +2,9 @@ use super::Color;
use enum_map::EnumMap;
use toml;
use std::collections::HashMap;
use std::ops::{Index, IndexMut};
/// Color configuration for the application.
///
/// Assign each color role an actual color.
@ -11,17 +14,112 @@ use toml;
/// # Example
///
/// ```rust
/// # use cursive::theme;
/// # use cursive::theme::Palette;
/// use cursive::theme::PaletteColor::*;
/// use cursive::theme::Color::*;
/// use cursive::theme::BaseColor::*;
///
/// let mut palette = theme::default_palette();
/// let mut palette = Palette::default();
///
/// assert_eq!(palette[Background], Dark(Blue));
/// palette[Shadow] = Light(Red);
/// ```
pub type Palette = EnumMap<PaletteColor, Color>;
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct Palette {
basic: EnumMap<PaletteColor, Color>,
custom: HashMap<String, PaletteNode>,
}
/// A node in the palette tree.
///
/// This can either be a color, or a nested namespace with its own mapping.
#[derive(PartialEq, Eq, Clone, Debug)]
enum PaletteNode {
Color(Color),
Namespace(HashMap<String, PaletteNode>),
}
// Basic usage: only use basic colors
impl Index<PaletteColor> for Palette {
type Output = Color;
fn index(&self, palette_color: PaletteColor) -> &Color {
&self.basic[palette_color]
}
}
// We can alter existing color if needed (but why?...)
impl IndexMut<PaletteColor> for Palette {
fn index_mut(&mut self, palette_color: PaletteColor) -> &mut Color {
&mut self.basic[palette_color]
}
}
impl Palette {
/// Returns a custom color from this palette.
///
/// Returns `None` if the given key was not found.
pub fn custom<'a>(&'a self, key: &str) -> Option<&'a Color> {
self.custom.get(key).and_then(|node| {
if let &PaletteNode::Color(ref color) = node {
Some(color)
} else {
None
}
})
}
/// Returns a new palette where the given namespace has been merged.
///
/// All values in the namespace will override previous values.
pub fn merge(&self, namespace: &str) -> Palette {
let mut result = self.clone();
if let Some(&PaletteNode::Namespace(ref palette)) =
self.custom.get(namespace)
{
// Merge `result` and `palette`
for (key, value) in palette.iter() {
match *value {
PaletteNode::Color(color) => result.set_color(key, color),
PaletteNode::Namespace(ref map) =>
result.add_namespace(key, map.clone()),
}
}
}
result
}
/// Sets the color for the given key.
///
/// This will update either the basic palette or the custom values.
pub fn set_color(&mut self, key: &str, color: Color) {
use theme::PaletteColor::*;
match key {
"background" => self.basic[Background] = color,
"shadow" => self.basic[Shadow] = color,
"view" => self.basic[View] = color,
"primary" => self.basic[Primary] = color,
"secondary" => self.basic[Secondary] = color,
"tertiary" => self.basic[Tertiary] = color,
"title_primary" => self.basic[TitlePrimary] = color,
"title_secondary" => self.basic[TitleSecondary] = color,
"highlight" => self.basic[Highlight] = color,
"highlight_inactive" => self.basic[HighlightInactive] = color,
other => {
self.custom
.insert(other.to_string(), PaletteNode::Color(color));
}
}
}
/// Adds a color namespace to this palette.
fn add_namespace(&mut self, key: &str, namespace: HashMap<String, PaletteNode>) {
self.custom.insert(key.to_string(), PaletteNode::Namespace(namespace));
}
}
/// Returns the default palette for a cursive application.
///
@ -35,57 +133,77 @@ pub type Palette = EnumMap<PaletteColor, Color>;
/// * `TitleSecondary` => `Dark(Yellow)`
/// * `Highlight` => `Dark(Red)`
/// * `HighlightInactive` => `Dark(Blue)`
pub fn default_palette() -> Palette {
use self::PaletteColor::*;
use theme::BaseColor::*;
use theme::Color::*;
impl Default for Palette {
fn default() -> Palette {
use self::PaletteColor::*;
use theme::BaseColor::*;
use theme::Color::*;
enum_map!{
Background => Dark(Blue),
Shadow => Dark(Black),
View => Dark(White),
Primary => Dark(Black),
Secondary => Dark(Blue),
Tertiary => Dark(White),
TitlePrimary => Dark(Red),
TitleSecondary => Dark(Yellow),
Highlight => Dark(Red),
HighlightInactive => Dark(Blue),
Palette {
basic: enum_map!{
Background => Dark(Blue),
Shadow => Dark(Black),
View => Dark(White),
Primary => Dark(Black),
Secondary => Dark(Blue),
Tertiary => Dark(White),
TitlePrimary => Dark(Red),
TitleSecondary => Dark(Yellow),
Highlight => Dark(Red),
HighlightInactive => Dark(Blue),
},
custom: HashMap::new(),
}
}
}
// Iterate over a toml
fn iterate_toml<'a>(table: &'a toml::value::Table) -> impl Iterator<Item=(&'a str, PaletteNode)> + 'a {
table.iter().flat_map(|(key, value)| {
let node = match value {
toml::Value::Table(table) => {
// This should define a new namespace
// Treat basic colors as simple string.
// We'll convert them back in the merge method.
let map = iterate_toml(table).map(|(key, value)| (key.to_string(), value)).collect();
// Should we only return something if it's non-empty?
Some(PaletteNode::Namespace(map))
}
toml::Value::Array(colors) => {
// This should be a list of colors - just pick the first valid one.
colors
.iter()
.flat_map(toml::Value::as_str)
.flat_map(Color::parse)
.map(PaletteNode::Color)
.next()
}
toml::Value::String(color) => {
// This describe a new color - easy!
Color::parse(color).map(PaletteNode::Color)
}
other => {
// Other - error?
debug!("Found unexpected value in theme: {} = {:?}", key, other);
None
}
};
node.map(|node| (key.as_str(), node))
})
}
/// Fills `palette` with the colors from the given `table`.
pub(crate) fn load_table(palette: &mut Palette, table: &toml::value::Table) {
pub(crate) fn load_toml(palette: &mut Palette, table: &toml::value::Table) {
// TODO: use serde for that?
// Problem: toml-rs doesn't do well with Enums...
load_color(
&mut palette[PaletteColor::Background],
table.get("background"),
);
load_color(&mut palette[PaletteColor::Shadow], table.get("shadow"));
load_color(&mut palette[PaletteColor::View], table.get("view"));
load_color(&mut palette[PaletteColor::Primary], table.get("primary"));
load_color(
&mut palette[PaletteColor::Secondary],
table.get("secondary"),
);
load_color(&mut palette[PaletteColor::Tertiary], table.get("tertiary"));
load_color(
&mut palette[PaletteColor::TitlePrimary],
table.get("title_primary"),
);
load_color(
&mut palette[PaletteColor::TitleSecondary],
table.get("title_secondary"),
);
load_color(
&mut palette[PaletteColor::Highlight],
table.get("highlight"),
);
load_color(
&mut palette[PaletteColor::HighlightInactive],
table.get("highlight_inactive"),
);
for (key, value) in iterate_toml(table) {
match value {
PaletteNode::Color(color) => palette.set_color(key, color),
PaletteNode::Namespace(map) => palette.add_namespace(key, map),
}
}
}
/// Color entry in a palette.
@ -121,25 +239,3 @@ impl PaletteColor {
palette[self]
}
}
/// Parses `value` and fills `target` if it's a valid color.
fn load_color(target: &mut Color, value: Option<&toml::Value>) -> bool {
if let Some(value) = value {
match *value {
toml::Value::String(ref value) => {
if let Some(color) = Color::parse(value) {
*target = color;
true
} else {
false
}
}
toml::Value::Array(ref array) => {
array.iter().any(|item| load_color(target, Some(item)))
}
_ => false,
}
} else {
false
}
}