More ncurses isolation

Also some renaming in the theme module
This commit is contained in:
Alexandre Bury 2016-06-30 23:38:01 -07:00
parent 4dcb2ea901
commit 985009e51c
14 changed files with 260 additions and 245 deletions

View File

@ -6,24 +6,24 @@ borders = "simple" # Alternatives are "none" and "outset"
# Base colors are red, green, blue,
# cyan, magenta, yellow, white and black.
[colors]
background = ["#923456", "magenta"]
background = ["454", "#923456", "401", "magenta"]
# If the value is an array, the first valid color will be used.
# If the terminal doesn't support custom color,
# non-base colors will be skipped.
shadow = ["#222222", "blue"]
view = "#888888"
shadow = ["#222288", "blue"]
view = "111"
# Array and simple values have the same effect.
primary = ["#111111"]
primary = ["white"]
secondary = "#EEEEEE"
tertiary = "#444444"
# Hex values can use lower or uppercase.
# (base color MUST be lowercase)
title_primary = "#883333"
title_primary = "yellow"
title_secondary = "#ffff55"
# Lower precision values can use only 3 digits.
highlight = "#833"
highlight = "#F88"
highlight_inactive = "#5555FF"

View File

@ -1,31 +1,15 @@
use backend;
use event;
use theme;
use utf8;
use ncurses;
pub trait Backend {
fn init();
fn finish();
fn clear();
fn refresh();
fn print_at((usize, usize), &str);
fn poll_event() -> event::Event;
fn set_refresh_rate(fps: u32);
fn screen_size() -> (usize, usize);
fn with_color<F: FnOnce()>(color: theme::ColorPair, f: F);
fn with_effect<F: FnOnce()>(effect: theme::Effect, f: F);
}
pub struct NcursesBackend;
impl Backend for NcursesBackend {
impl backend::Backend for NcursesBackend {
fn init() {
::std::env::set_var("ESCDELAY", "25");
ncurses::setlocale(ncurses::LcCategory::all, "");
ncurses::initscr();
ncurses::keypad(ncurses::stdscr, true);
@ -34,7 +18,7 @@ impl Backend for NcursesBackend {
ncurses::start_color();
ncurses::curs_set(ncurses::CURSOR_VISIBILITY::CURSOR_INVISIBLE);
ncurses::wbkgd(ncurses::stdscr,
ncurses::COLOR_PAIR(theme::ColorPair::Background.ncurses_id()));
ncurses::COLOR_PAIR(theme::ColorStyle::Background.id()));
}
fn screen_size() -> (usize, usize) {
@ -44,16 +28,31 @@ impl Backend for NcursesBackend {
(x as usize, y as usize)
}
fn has_colors() -> bool {
ncurses::has_colors()
}
fn finish() {
ncurses::endwin();
}
fn with_color<F: FnOnce()>(color: theme::ColorPair, f: F) {
fn init_color_style(style: theme::ColorStyle,
foreground: &theme::Color,
background: &theme::Color) {
// TODO: build the color on the spot
ncurses::init_pair(style.id(),
find_closest(foreground) as i16,
find_closest(background) as i16);
}
fn with_color<F: FnOnce()>(color: theme::ColorStyle, f: F) {
let mut current_style: ncurses::attr_t = 0;
let mut current_color: i16 = 0;
ncurses::attr_get(&mut current_style, &mut current_color);
let style = ncurses::COLOR_PAIR(color.ncurses_id());
let style = ncurses::COLOR_PAIR(color.id());
ncurses::attron(style);
f();
// ncurses::attroff(style);
@ -218,3 +217,25 @@ fn parse_ncurses_char(ch: i32) -> event::Key {
_ => event::Key::Unknown(ch),
}
}
fn find_closest(color: &theme::Color) -> u8 {
match *color {
theme::Color::Black => 0,
theme::Color::Red => 1,
theme::Color::Green => 2,
theme::Color::Yellow => 3,
theme::Color::Blue => 4,
theme::Color::Magenta => 5,
theme::Color::Cyan => 6,
theme::Color::White => 7,
theme::Color::Rgb(r, g, b) => {
let r = 6 * r as u16 / 256;
let g = 6 * g as u16 / 256;
let b = 6 * b as u16 / 256;
(16 + 36 * r + 6 * g + b) as u8
}
theme::Color::RgbLowRes(r, g, b) => {
(16 + 36 * r + 6 * g + b) as u8
}
}
}

31
src/backend/mod.rs Normal file
View File

@ -0,0 +1,31 @@
use event;
use theme;
// Module is not named `ncurses` to avoir naming conflict
mod curses;
pub use self::curses::NcursesBackend;
pub trait Backend {
fn init();
fn finish();
fn clear();
fn refresh();
fn has_colors() -> bool;
fn init_color_style(style: theme::ColorStyle,
foreground: &theme::Color,
background: &theme::Color);
fn print_at((usize, usize), &str);
fn poll_event() -> event::Event;
fn set_refresh_rate(fps: u32);
fn screen_size() -> (usize, usize);
fn with_color<F: FnOnce()>(color: theme::ColorStyle, f: F);
fn with_effect<F: FnOnce()>(effect: theme::Effect, f: F);
}

View File

@ -90,13 +90,11 @@ impl Cursive {
/// Creates a new Cursive root, and initialize ncurses.
pub fn new() -> Self {
// Default delay is way too long. 25 is imperceptible yet works fine.
std::env::set_var("ESCDELAY", "25");
B::init();
let theme = theme::load_default();
// let theme = theme::load_theme("assets/style.toml").unwrap();
let mut res = Cursive {
theme: theme,
screens: Vec::new(),

View File

@ -1,5 +1,5 @@
use menu::*;
use theme::ColorPair;
use theme::ColorStyle;
use printer::Printer;
use event::*;
@ -22,7 +22,7 @@ impl Menubar {
pub fn draw(&mut self, printer: &Printer) {
// Draw the bar at the top
printer.with_color(ColorPair::Primary, |printer| {
printer.with_color(ColorStyle::Primary, |printer| {
printer.print_hline((0, 0), printer.size.x, " ");
});

View File

@ -6,7 +6,7 @@ use backend::Backend;
use B;
use theme::{ColorPair, Theme, Effect};
use theme::{ColorStyle, Theme, Effect};
use vec::{ToVec2, Vec2};
/// Convenient interface to draw on a subset of the screen.
@ -94,11 +94,11 @@ impl Printer {
/// # use cursive::printer::Printer;
/// # use cursive::theme;
/// # let printer = Printer::new((6,4), theme::load_default());
/// printer.with_color(theme::ColorPair::Highlight, |printer| {
/// printer.with_color(theme::ColorStyle::Highlight, |printer| {
/// printer.print((0,0), "This text is highlighted!");
/// });
/// ```
pub fn with_color<F>(&self, c: ColorPair, f: F)
pub fn with_color<F>(&self, c: ColorStyle, f: F)
where F: FnOnce(&Printer)
{
B::with_color(c, || f(self));

View File

@ -5,17 +5,24 @@ use std::io::Read;
use std::fs::File;
use std::path::Path;
use ncurses;
use backend::Backend;
use toml;
use B;
pub enum Effect {
Simple,
Reverse,
}
/// Represents the color of a character and its background.
/// Possible color style for a cell.
///
/// Represents a color pair role to use when printing something.
///
/// The current theme will assign each role a foreground and background color.
#[derive(Clone,Copy)]
pub enum ColorPair {
pub enum ColorStyle {
/// Application background, where no view is present.
Background,
/// Color used by view shadows. Only background matters.
@ -36,19 +43,19 @@ pub enum ColorPair {
HighlightInactive,
}
impl ColorPair {
impl ColorStyle {
/// Returns the ncurses pair ID associated with this color pair.
pub fn ncurses_id(self) -> i16 {
pub fn id(self) -> i16 {
match self {
ColorPair::Background => 1,
ColorPair::Shadow => 2,
ColorPair::Primary => 3,
ColorPair::Secondary => 4,
ColorPair::Tertiary => 5,
ColorPair::TitlePrimary => 6,
ColorPair::TitleSecondary => 7,
ColorPair::Highlight => 8,
ColorPair::HighlightInactive => 9,
ColorStyle::Background => 1,
ColorStyle::Shadow => 2,
ColorStyle::Primary => 3,
ColorStyle::Secondary => 4,
ColorStyle::Tertiary => 5,
ColorStyle::TitlePrimary => 6,
ColorStyle::TitleSecondary => 7,
ColorStyle::Highlight => 8,
ColorStyle::HighlightInactive => 9,
}
}
}
@ -61,29 +68,31 @@ pub struct Theme {
/// How view borders should be drawn.
pub borders: BorderStyle,
/// What colors should be used through the application?
pub colors: ColorStyle,
pub colors: Palette,
}
impl Theme {
fn default() -> Theme {
impl Default for Theme {
fn default() -> Self {
Theme {
shadow: true,
borders: BorderStyle::Simple,
colors: ColorStyle {
background: Color::blue(),
shadow: Color::black(),
view: Color::white(),
primary: Color::black(),
secondary: Color::blue(),
tertiary: Color::white(),
title_primary: Color::red(),
title_secondary: Color::yellow(),
highlight: Color::red(),
highlight_inactive: Color::blue(),
colors: Palette {
background: Color::Blue,
shadow: Color::Black,
view: Color::White,
primary: Color::Black,
secondary: Color::Blue,
tertiary: Color::White,
title_primary: Color::Red,
title_secondary: Color::Yellow,
highlight: Color::Red,
highlight_inactive: Color::Blue,
},
}
}
}
impl Theme {
fn load(&mut self, table: &toml::Table) {
if let Some(&toml::Value::Boolean(shadow)) = table.get("shadow") {
self.shadow = shadow;
@ -100,45 +109,48 @@ impl Theme {
}
}
fn apply(&self) {
Theme::apply_color(ColorPair::Background,
fn activate(&self) {
// Initialize each color with the backend
B::init_color_style(ColorStyle::Background,
&self.colors.view,
&self.colors.background);
Theme::apply_color(ColorPair::Shadow, &self.colors.shadow, &self.colors.shadow);
Theme::apply_color(ColorPair::Primary, &self.colors.primary, &self.colors.view);
Theme::apply_color(ColorPair::Secondary,
B::init_color_style(ColorStyle::Shadow,
&self.colors.shadow,
&self.colors.shadow);
B::init_color_style(ColorStyle::Primary,
&self.colors.primary,
&self.colors.view);
B::init_color_style(ColorStyle::Secondary,
&self.colors.secondary,
&self.colors.view);
Theme::apply_color(ColorPair::Tertiary,
B::init_color_style(ColorStyle::Tertiary,
&self.colors.tertiary,
&self.colors.view);
Theme::apply_color(ColorPair::TitlePrimary,
B::init_color_style(ColorStyle::TitlePrimary,
&self.colors.title_primary,
&self.colors.view);
Theme::apply_color(ColorPair::TitleSecondary,
B::init_color_style(ColorStyle::TitleSecondary,
&self.colors.title_secondary,
&self.colors.view);
Theme::apply_color(ColorPair::Highlight,
B::init_color_style(ColorStyle::Highlight,
&self.colors.view,
&self.colors.highlight);
Theme::apply_color(ColorPair::HighlightInactive,
B::init_color_style(ColorStyle::HighlightInactive,
&self.colors.view,
&self.colors.highlight_inactive);
}
fn apply_color(pair: ColorPair, front: &Color, back: &Color) {
ncurses::init_pair(pair.ncurses_id(), front.id, back.id);
}
}
/// Specifies how View borders should be drawn.
/// Specifies how some borders should be drawn.
///
/// Borders are used around Dialogs, select popups, and panels.
#[derive(Clone,Copy,Debug)]
pub enum BorderStyle {
/// Don't draw any border.
NoBorder,
/// Simple borders.
Simple,
/// Outset borders with a 3d effect.
/// Outset borders with a simple 3d effect.
Outset,
}
@ -156,9 +168,11 @@ impl BorderStyle {
}
}
/// Represents the colors the application will use in various situations.
/// Color configuration for the application.
///
/// Assign each color role an actual color.
#[derive(Clone,Debug)]
pub struct ColorStyle {
pub struct Palette {
/// Color used for the application background.
pub background: Color,
/// Color used for View shadows.
@ -181,39 +195,57 @@ pub struct ColorStyle {
pub highlight_inactive: Color,
}
impl ColorStyle {
impl Palette {
fn load(&mut self, table: &toml::Table) {
let mut new_id = 16;
self.background.load(table, "background", &mut new_id);
self.shadow.load(table, "shadow", &mut new_id);
self.view.load(table, "view", &mut new_id);
self.primary.load(table, "primary", &mut new_id);
self.secondary.load(table, "secondary", &mut new_id);
self.tertiary.load(table, "tertiary", &mut new_id);
self.title_primary.load(table, "title_primary", &mut new_id);
self.title_secondary.load(table, "title_secondary", &mut new_id);
self.highlight.load(table, "highlight", &mut new_id);
self.highlight_inactive.load(table, "highlight_inactive", &mut new_id);
load_color(&mut self.background, table.get("background"));
load_color(&mut self.shadow, table.get("shadow"));
load_color(&mut self.view, table.get("view"));
load_color(&mut self.primary, table.get("primary"));
load_color(&mut self.secondary, table.get("secondary"));
load_color(&mut self.tertiary, table.get("tertiary"));
load_color(&mut self.title_primary, table.get("title_primary"));
load_color(&mut self.title_secondary, table.get("title_secondary"));
load_color(&mut self.highlight, table.get("highlight"));
load_color(&mut self.highlight_inactive, table.get("highlight_inactive"));
}
}
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
}
}
/// Represents a color used by the theme.
#[derive(Clone,Debug)]
pub struct Color {
pub enum Color {
/// Color ID used by ncurses.
pub id: i16,
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
// 24-bit color
Rgb(u8, u8, u8),
RgbLowRes(u8, u8, u8),
}
impl Color {
/// Return the rgb values used by the color.
pub fn rgb(&self) -> (i16, i16, i16) {
let (mut r, mut g, mut b) = (0, 0, 0);
ncurses::color_content(self.id, &mut r, &mut g, &mut b);
(r, g, b)
}
}
/// Possible error returned when loading a theme.
@ -232,114 +264,46 @@ impl From<io::Error> for Error {
}
impl Color {
fn parse(value: &str, new_id: &mut i16) -> Option<Self> {
let color = if value == "black" {
Color::black()
} else if value == "red" {
Color::red()
} else if value == "green" {
Color::green()
} else if value == "yellow" {
Color::yellow()
} else if value == "blue" {
Color::blue()
} else if value == "magenta" {
Color::magenta()
} else if value == "cyan" {
Color::cyan()
} else if value == "white" {
Color::white()
fn parse(value: &str) -> Option<Self> {
Some(match value {
"black" => Color::Black,
"red" => Color::Red,
"green" => Color::Green,
"yellow" => Color::Yellow,
"blue" => Color::Blue,
"magenta" => Color::Magenta,
"cyan" => Color::Cyan,
"white" => Color::White,
value => return Color::parse_special(value),
})
}
fn parse_special(value: &str) -> Option<Color> {
if value.starts_with('#') {
let value = &value[1..];
// Compute per-color length, and amplitude
let (l, multiplier) = match value.len() {
6 => (2, 1),
3 => (1, 17),
_ => panic!("Cannot parse color: {}", value),
};
let r = load_hex(&value[0 * l..1 * l]) * multiplier;
let g = load_hex(&value[1 * 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?
let rgb: Vec<_> = value.chars().map(|c| c as i16 - '0' as i16).collect();
println!("{:?}", rgb);
if rgb.iter().all(|&i| i >= 0 && i < 6) {
Some(Color::RgbLowRes(rgb[0] as u8, rgb[1] as u8, rgb[2] as u8))
} else {
// Let's make a new color
return Color::make_new(value, new_id);
};
Some(color)
None
}
fn load(&mut self, table: &toml::Table, key: &str, new_id: &mut i16) {
match table.get(key) {
Some(&toml::Value::String(ref value)) => {
self.load_value(value, new_id);
} else {
None
}
Some(&toml::Value::Array(ref array)) => {
for color in array.iter() {
if let toml::Value::String(ref color) = *color {
if self.load_value(color, new_id) {
return;
}
}
}
}
_ => (),
}
}
fn load_value(&mut self, value: &str, new_id: &mut i16) -> bool {
match Color::parse(value, new_id) {
Some(color) => self.id = color.id,
None => return false,
}
true
}
fn make_new(value: &str, new_id: &mut i16) -> Option<Self> {
// if !ncurses::can_change_color() {
if !ncurses::has_colors() {
return None;
}
if !value.starts_with('#') {
return None;
}
if *new_id >= ncurses::COLORS as i16 {
return None;
}
let s = &value[1..];
let (l, max) = match s.len() {
6 => (2, 255),
3 => (1, 15),
_ => panic!("Cannot parse color: {}", s),
};
let r = (load_hex(&s[0 * l..1 * l]) as i32 * 1000 / max) as i16;
let g = (load_hex(&s[1 * l..2 * l]) as i32 * 1000 / max) as i16;
let b = (load_hex(&s[2 * l..3 * l]) as i32 * 1000 / max) as i16;
ncurses::init_color(*new_id, r, g, b);
let color = Color { id: *new_id };
*new_id += 1;
Some(color)
}
pub fn black() -> Self {
Color { id: 0 }
}
pub fn red() -> Self {
Color { id: 1 }
}
pub fn green() -> Self {
Color { id: 2 }
}
pub fn yellow() -> Self {
Color { id: 3 }
}
pub fn blue() -> Self {
Color { id: 4 }
}
pub fn magenta() -> Self {
Color { id: 5 }
}
pub fn cyan() -> Self {
Color { id: 6 }
}
pub fn white() -> Self {
Color { id: 7 }
}
}
@ -379,6 +343,7 @@ impl Color {
/// highlight_inactive = "#5555FF"
/// ```
/// Loads a theme and sets it as active.
pub fn load_theme<P: AsRef<Path>>(filename: P) -> Result<Theme, Error> {
let content = {
let mut content = String::new();
@ -395,7 +360,7 @@ pub fn load_theme<P: AsRef<Path>>(filename: P) -> Result<Theme, Error> {
let mut theme = Theme::default();
theme.load(&table);
theme.apply();
theme.activate();
Ok(theme)
}
@ -403,12 +368,12 @@ pub fn load_theme<P: AsRef<Path>>(filename: P) -> Result<Theme, Error> {
/// Loads the default theme, and returns its representation.
pub fn load_default() -> Theme {
let theme = Theme::default();
theme.apply();
theme.activate();
theme
}
/// Loads a hexadecimal code
fn load_hex(s: &str) -> i16 {
fn load_hex(s: &str) -> u16 {
let mut sum = 0;
for c in s.chars() {
sum *= 16;
@ -420,5 +385,5 @@ fn load_hex(s: &str) -> i16 {
};
}
sum
sum as u16
}

View File

@ -1,6 +1,6 @@
use std::rc::Rc;
use theme::ColorPair;
use theme::ColorStyle;
use Cursive;
use vec::Vec2;
use view::{SizeRequest, View};
@ -29,9 +29,9 @@ impl Button {
impl View for Button {
fn draw(&mut self, printer: &Printer) {
let style = if !printer.focused {
ColorPair::Primary
ColorStyle::Primary
} else {
ColorPair::Highlight
ColorStyle::Highlight
};
let x = printer.size.x - 1;

View File

@ -4,7 +4,7 @@ use std::any::Any;
use Cursive;
use align::*;
use event::*;
use theme::ColorPair;
use theme::ColorStyle;
use view::{DimensionRequest, Selector, SizeRequest, TextView, View};
use view::{Button, SizedView};
use vec::{ToVec4, Vec2, Vec4};
@ -152,7 +152,7 @@ impl View for Dialog {
printer.print((x - 2, 0), "");
printer.print((x + len, 0), "");
printer.with_color(ColorPair::TitlePrimary, |p| p.print((x, 0), &self.title));
printer.with_color(ColorStyle::TitlePrimary, |p| p.print((x, 0), &self.title));
}
}

View File

@ -2,7 +2,7 @@ use unicode_segmentation::UnicodeSegmentation;
use std::cmp::min;
use theme::{ColorPair, Effect};
use theme::{ColorStyle, Effect};
use vec::Vec2;
use view::{IdView, SizeRequest, View};
use event::*;
@ -85,7 +85,7 @@ impl View for EditView {
fn draw(&mut self, printer: &Printer) {
// let style = if focused { color::HIGHLIGHT } else { color::HIGHLIGHT_INACTIVE };
let len = self.content.chars().count();
printer.with_color(ColorPair::Secondary, |printer| {
printer.with_color(ColorStyle::Secondary, |printer| {
printer.with_effect(Effect::Reverse, |printer| {
if len < self.last_length {
printer.print((0, 0), &self.content);

View File

@ -1,6 +1,6 @@
use std::cmp::{max, min};
use theme::ColorPair;
use theme::ColorStyle;
use vec::Vec2;
use printer::Printer;
@ -129,9 +129,9 @@ impl ScrollBase {
let start = steps * self.start_line / (1 + self.content_height - self.view_height);
let color = if printer.focused {
ColorPair::Highlight
ColorStyle::Highlight
} else {
ColorPair::HighlightInactive
ColorStyle::HighlightInactive
};
printer.print_vline((printer.size.x - 1, 0), printer.size.y, "|");

View File

@ -1,7 +1,7 @@
use std::cmp::min;
use std::rc::Rc;
use theme::ColorPair;
use theme::ColorStyle;
use Cursive;
use align::*;
use view::{DimensionRequest, IdView, SizeRequest, View};
@ -133,12 +133,12 @@ impl<T: 'static> View for SelectView<T> {
self.scrollbase.draw(printer, |printer, i| {
let style = if i == self.focus {
if printer.focused {
ColorPair::Highlight
ColorStyle::Highlight
} else {
ColorPair::HighlightInactive
ColorStyle::HighlightInactive
}
} else {
ColorPair::Primary
ColorStyle::Primary
};
printer.with_color(style, |printer| {
let l = self.items[i].label.chars().count();

View File

@ -1,7 +1,7 @@
use view::{SizeRequest, View, ViewWrapper};
use printer::Printer;
use vec::Vec2;
use theme::ColorPair;
use theme::ColorStyle;
/// Wrapper view that adds a shadow.
///
@ -40,7 +40,7 @@ impl<T: View> ViewWrapper for ShadowView<T> {
let h = printer.size.y - 1;
let w = printer.size.x - 1;
printer.with_color(ColorPair::Shadow, |printer| {
printer.with_color(ColorStyle::Shadow, |printer| {
printer.print_hline((2, h), w - 1, " ");
printer.print_vline((w, 2), h - 1, " ");
});

View File

@ -4,7 +4,7 @@ use vec::Vec2;
use view::{DimensionRequest, Selector, ShadowView, SizeRequest, View};
use event::{Event, EventResult};
use printer::Printer;
use theme::ColorPair;
use theme::ColorStyle;
/// Simple stack of views.
/// Only the top-most view is active and can receive input.
@ -49,7 +49,7 @@ impl StackView {
impl View for StackView {
fn draw(&mut self, printer: &Printer) {
let last = self.layers.len();
printer.with_color(ColorPair::Primary, |printer| {
printer.with_color(ColorStyle::Primary, |printer| {
for (i, v) in self.layers.iter_mut().enumerate() {
// Center the view
let size = v.size;