Add circleci and codecov (#13)

* Add circleci and codecov

* Update version

* Fix format test

* Fix CI

* Add tarpaulin

* Format code

* Fix tarpaulin

* lexers tests

* Remove non-lexer token type, add more tests

* More tests

* More tests

* Refactor, some test, split code, add more traits

* Add codecov config

* Fix code style

* Test text character

* Menu bar tests

* Caret tests

* Refactor caret, more caret tests
This commit is contained in:
Adrian Woźniak 2019-01-07 21:46:01 +01:00 committed by GitHub
parent 0cf8629868
commit 4e13b4d2d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 2222 additions and 656 deletions

54
.circleci/config.yml Normal file
View File

@ -0,0 +1,54 @@
version: 2.1
jobs:
build:
working_directory: ~/rider
docker:
- image: abronan/rust-circleci:latest
environment:
CODECOV_TOKEN: "e58da505-19f2-481c-8068-e845cb36fbe4"
TZ: "/usr/share/zoneinfo/Europe/Paris"
steps:
- checkout
- restore_cache:
key: project-cache
- run:
name: Install dependencies
command: |
apt-get update
apt-get install -y -qq xvfb mesa-utils mesa-vdpau-drivers
apt-get install -y libssl-dev pkg-config cmake zlib1g-dev
apt-get install -q -y libsdl2-dev libsdl2-2.0-0 libsdl2-gfx-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-net-dev libsdl2-ttf-dev
- run:
name: Check formatting
command: |
rustfmt --version
cargo fmt -- --error-on-unformatted --unstable-features --check
- run:
name: Nightly Build
command: |
rustup run nightly rustc --version --verbose
rustup run nightly cargo --version --verbose
rustup run nightly cargo build
- run:
name: Test and code coverage
command: |
RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin
Xvfb :5 -screen 0 800x600x24 +extension GLX +render -noreset &
export DISPLAY=:5
export XDG_RUNTIME_DIR=$(pwd)
rustup run nightly cargo tarpaulin --ciserver circle-ci --out Xml
- run:
name: Upload Coverage
command: |
wget -O - -q "https://codecov.io/bash" > .codecov
chmod +x .codecov
./.codecov -t $CODECOV_TOKEN
echo "Uploaded code coverage"
- save_cache:
key: project-cache
paths:
- "~/.cargo"
- "./target"

18
.codecov.yml Normal file
View File

@ -0,0 +1,18 @@
codecov:
notify:
require_ci_to_pass: yes
coverage:
precision: 2
round: down
range: "70...100"
status:
project: yes
patch: yes
changes: yes
comment:
layout: "header, diff"
behavior: default
require_changes: no

2
.gitignore vendored
View File

@ -3,3 +3,5 @@
.idea
log
.rider
.codecov
cobertura.xml

1
Cargo.lock generated
View File

@ -416,6 +416,7 @@ version = "0.1.0"
dependencies = [
"dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"plex 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -14,6 +14,7 @@ serde_derive = "*"
log = "*"
env_logger = "*"
simplelog = "*"
lazy_static = "*"
[dependencies.sdl2]
version = "0.31.0"

View File

@ -18,8 +18,9 @@ Text editor in rust
* [ ] Lock scroll when no available content
* [ ] Config edit menu
* [ ] Project tree
* [ ] Cover `rider` with tests
* [ ] Cover `rider` with tests at least 50%
* [x] Handle resize window
* [ ] Selection
### v1.1
@ -29,6 +30,8 @@ Text editor in rust
* [ ] Git support
* [ ] Context menu
* [ ] Keep indent
* [ ] Multi-selection
* [ ] Cover `rider` with tests at least 75%
### v1.2
* [ ] Multi-caret

130
guitest/main.rs Normal file
View File

@ -0,0 +1,130 @@
#[cfg(test)]
mod file_editor {
use sdl2::pixels::*;
use sdl2::rect::*;
use std::sync::*;
#[test]
fn add_text() {
use crate::tests::support;
let config = support::build_config();
let canvas = support::build_canvas();
let font_context = sdl2::ttf::init().unwrap();
let texture_creator = canvas.texture_creator();
let mut renderer = Renderer::new(Arc::clone(&config), &font_context, &texture_creator);
let mut editor = FileEditor::new(Arc::clone(&config));
let mut file = EditorFile::new("./foo.txt".to_string(), "foo".to_string(), config.clone());
file.prepare_ui(&mut renderer);
assert_eq!(editor.open_file(file).is_none(), true);
assert_eq!(editor.caret().position().text_position(), 0);
assert_eq!(editor.file().is_some(), true);
assert_eq!(editor.file().unwrap().sections().len(), 1);
assert_eq!(editor.file().unwrap().get_character_at(0).is_some(), true);
editor.insert_text("z".to_string(), &mut renderer);
assert_eq!(editor.caret().position().text_position(), 1);
assert_eq!(editor.file().is_some(), true);
assert_eq!(editor.file().unwrap().buffer(), "zfoo".to_string());
}
}
#[cfg(test)]
mod text_character {
use sdl2::pixels::*;
use sdl2::rect::*;
use std::sync::*;
#[test]
fn must_return_valid_source() {
let config = support::build_config();
let canvas = support::build_canvas();
let font_context = sdl2::ttf::init().unwrap();
let texture_creator = canvas.texture_creator();
let mut renderer = Renderer::new(Arc::clone(&config), &font_context, &texture_creator);
let mut widget =
TextCharacter::new('\n', 0, 0, true, Color::RGB(1, 12, 123), Arc::clone(&config));
widget.prepare_ui(&mut renderer);
assert_eq!(widget.source(), &Rect::new(0, 0, 0, 0));
}
#[test]
fn must_return_valid_dest() {
let config = support::build_config();
let canvas = support::build_canvas();
let font_context = sdl2::ttf::init().unwrap();
let texture_creator = canvas.texture_creator();
let mut renderer = Renderer::new(Arc::clone(&config), &font_context, &texture_creator);
let mut widget =
TextCharacter::new('\n', 0, 0, true, Color::RGB(1, 12, 123), Arc::clone(&config));
widget.prepare_ui(&mut renderer);
assert_eq!(widget.dest(), &Rect::new(0, 0, 0, 0));
}
#[test]
fn must_return_valid_color() {
let config = support::build_config();
let canvas = support::build_canvas();
let font_context = sdl2::ttf::init().unwrap();
let texture_creator = canvas.texture_creator();
let mut renderer = Renderer::new(Arc::clone(&config), &font_context, &texture_creator);
let mut widget =
TextCharacter::new('\n', 0, 0, true, Color::RGB(1, 12, 123), Arc::clone(&config));
widget.prepare_ui(&mut renderer);
assert_eq!(widget.color(), &Color::RGB(1, 12, 123));
}
#[test]
fn must_update_position_of_new_line() {
let config = support::build_config();
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
let window = video_subsystem
.window("Test", 1, 1)
.borderless()
.opengl()
.build()
.unwrap();
let canvas = window.into_canvas().accelerated().build().unwrap();
let font_context = sdl2::ttf::init().unwrap();
let texture_creator = canvas.texture_creator();
let mut renderer = Renderer::new(Arc::clone(&config), &font_context, &texture_creator);
let mut widget =
TextCharacter::new('\n', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config));
widget.prepare_ui(&mut renderer);
let mut current = Rect::new(0, 0, 0, 0);
widget.update_position(&mut current);
assert_eq!(current, Rect::new(0, 0, 0, 0));
assert_eq!(widget.dest(), &Rect::new(0, 0, 0, 0));
}
}
#[cfg(test)]
fn main() {
let config = support::build_config();
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
let window = video_subsystem
.window("Test", 1, 1)
.borderless()
.opengl()
.build()
.unwrap();
let canvas = window.into_canvas().accelerated().build().unwrap();
}

View File

@ -1,10 +1,9 @@
use crate::app::caret_manager;
use crate::app::file_content_manager;
use crate::app::{UpdateResult, WindowCanvas as WC};
use crate::config::Config;
use crate::config::*;
use crate::renderer::Renderer;
use crate::ui::caret::Caret;
use crate::ui::caret::{CaretPosition, MoveDirection};
use crate::ui::caret::*;
use crate::ui::file::editor_file::EditorFile;
use crate::ui::file::*;
use crate::ui::menu_bar::MenuBar;
@ -48,10 +47,6 @@ impl AppState {
};
}
pub fn config(&self) -> &Arc<RwLock<Config>> {
&self.config
}
pub fn file_editor(&self) -> &FileEditor {
&self.file_editor
}
@ -62,9 +57,9 @@ impl AppState {
}
impl Render for AppState {
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, _parent: Parent) -> UpdateResult {
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, _parent: Parent) {
self.file_editor.render(canvas, renderer, None);
self.menu_bar.render(canvas, renderer, None)
self.menu_bar.render(canvas, renderer, None);
}
fn prepare_ui(&mut self, renderer: &mut Renderer) {
@ -108,3 +103,9 @@ impl AppState {
true
}
}
impl ConfigHolder for AppState {
fn config(&self) -> &ConfigAccess {
&self.config
}
}

View File

@ -1,5 +1,5 @@
pub use crate::app::app_state::AppState;
pub use crate::config::Config;
pub use crate::config::{Config, ConfigAccess, ConfigHolder};
pub use crate::renderer::Renderer;
use crate::themes::*;
use crate::ui::caret::{CaretPosition, MoveDirection};
@ -251,8 +251,10 @@ impl Application {
}
UpdateResult::NoOp
}
}
pub fn config(&self) -> &Arc<RwLock<Config>> {
impl ConfigHolder for Application {
fn config(&self) -> &ConfigAccess {
&self.config
}
}

114
src/config/config.rs Normal file
View File

@ -0,0 +1,114 @@
use crate::config::creator;
use crate::config::EditorConfig;
use crate::lexer::Language;
use crate::themes::Theme;
use dirs;
use std::collections::HashMap;
use std::fs;
pub type LanguageMapping = HashMap<String, Language>;
#[derive(Debug, Clone)]
pub struct Config {
width: u32,
height: u32,
scroll_speed: i32,
menu_height: u16,
editor_config: EditorConfig,
theme: Theme,
extensions_mapping: LanguageMapping,
}
impl Config {
pub fn new() -> Self {
creator::create();
let editor_config = EditorConfig::new();
let mut extensions_mapping = HashMap::new();
extensions_mapping.insert(".".to_string(), Language::PlainText);
extensions_mapping.insert("txt".to_string(), Language::PlainText);
extensions_mapping.insert("rs".to_string(), Language::Rust);
Self {
width: 1024,
height: 860,
scroll_speed: 10,
menu_height: 60,
theme: Theme::load(editor_config.current_theme().clone()),
editor_config,
extensions_mapping,
}
}
pub fn scroll_speed(&self) -> i32 {
self.scroll_speed
}
pub fn width(&self) -> u32 {
self.width
}
pub fn set_width(&mut self, w: u32) {
self.width = w;
}
pub fn height(&self) -> u32 {
self.height
}
pub fn set_height(&mut self, h: u32) {
self.height = h;
}
pub fn editor_config(&self) -> &EditorConfig {
&self.editor_config
}
pub fn theme(&self) -> &Theme {
&self.theme
}
pub fn menu_height(&self) -> u16 {
self.menu_height
}
pub fn editor_top_margin(&self) -> i32 {
(self.menu_height() as i32) + (self.editor_config().margin_top() as i32)
}
pub fn editor_left_margin(&self) -> i32 {
self.editor_config().margin_left() as i32
}
pub fn extensions_mapping(&self) -> &LanguageMapping {
&self.extensions_mapping
}
}
#[cfg(test)]
mod tests {
use crate::config::*;
use crate::lexer::*;
#[test]
fn must_return_language_mapping() {
let config = Config::new();
let mapping = config.extensions_mapping();
{
let mut keys: Vec<String> = mapping.keys().map(|s| s.to_string()).collect();
let mut expected: Vec<String> =
vec![".".to_string(), "txt".to_string(), "rs".to_string()];
keys.sort();
expected.sort();
assert_eq!(keys, expected);
}
{
let mut keys: Vec<Language> = mapping.values().map(|s| s.clone()).collect();
let mut expected: Vec<Language> =
vec![Language::PlainText, Language::PlainText, Language::Rust];
keys.sort();
expected.sort();
assert_eq!(keys, expected);
}
}
}

View File

@ -6,23 +6,28 @@ use std::path;
pub fn create() {
if !themes_dir().exists() {
fs::create_dir_all(&themes_dir())
.unwrap_or_else(|_| panic!("Cannot create themes config directory"));
let r = fs::create_dir_all(&themes_dir());
#[cfg_attr(tarpaulin, skip)]
r.unwrap_or_else(|_| panic!("Cannot create themes config directory"));
}
if !fonts_dir().exists() {
fs::create_dir_all(&fonts_dir())
.unwrap_or_else(|_| panic!("Cannot create fonts config directory"));
let r = fs::create_dir_all(&fonts_dir());
#[cfg_attr(tarpaulin, skip)]
r.unwrap_or_else(|_| panic!("Cannot create fonts config directory"));
write_default_fonts();
}
if !log_dir().exists() {
fs::create_dir_all(&log_dir()).unwrap_or_else(|_| panic!("Cannot create log directory"));
let r = fs::create_dir_all(&log_dir());
#[cfg_attr(tarpaulin, skip)]
r.unwrap_or_else(|_| panic!("Cannot create log directory"));
}
if !project_dir().exists() {
fs::create_dir_all(&project_dir())
.unwrap_or_else(|_| panic!("Cannot create project directory"));
let r = fs::create_dir_all(&project_dir());
#[cfg_attr(tarpaulin, skip)]
r.unwrap_or_else(|_| panic!("Cannot create project directory"));
}
}
@ -31,14 +36,16 @@ fn write_default_fonts() {
let mut default_font_path = fonts_dir();
default_font_path.push("DejaVuSansMono.ttf");
let contents = include_bytes!("../../assets/fonts/DejaVuSansMono.ttf");
fs::write(default_font_path, contents.to_vec())
.unwrap_or_else(|_| panic!("Cannot write default font file!"));
let r = fs::write(default_font_path, contents.to_vec());
#[cfg_attr(tarpaulin, skip)]
r.unwrap_or_else(|_| panic!("Cannot write default font file!"));
}
{
let mut default_font_path = fonts_dir();
default_font_path.push("ElaineSans-Medium.ttf");
let contents = include_bytes!("../../assets/fonts/ElaineSans-Medium.ttf");
fs::write(default_font_path, contents.to_vec())
.unwrap_or_else(|_| panic!("Cannot write default font file!"));
let r = fs::write(default_font_path, contents.to_vec());
#[cfg_attr(tarpaulin, skip)]
r.unwrap_or_else(|_| panic!("Cannot write default font file!"));
}
}

View File

@ -0,0 +1,44 @@
use crate::config::directories;
#[derive(Debug, Clone)]
pub struct EditorConfig {
character_size: u16,
font_path: String,
current_theme: String,
margin_left: u16,
margin_top: u16,
}
impl EditorConfig {
pub fn new() -> Self {
let mut default_font_path = directories::fonts_dir();
default_font_path.push("DejaVuSansMono.ttf");
Self {
character_size: 14,
font_path: default_font_path.to_str().unwrap().to_string(),
current_theme: "railscasts".to_string(),
margin_left: 10,
margin_top: 10,
}
}
pub fn character_size(&self) -> u16 {
self.character_size
}
pub fn font_path(&self) -> &String {
&self.font_path
}
pub fn current_theme(&self) -> &String {
&self.current_theme
}
pub fn margin_left(&self) -> u16 {
self.margin_left
}
pub fn margin_top(&self) -> u16 {
self.margin_top
}
}

View File

@ -1,129 +1,16 @@
use crate::lexer::Language;
use crate::themes::Theme;
use dirs;
use std::collections::HashMap;
use std::fs;
use std::sync::{Arc, RwLock};
mod creator;
pub mod config;
pub(crate) mod creator;
pub mod directories;
pub mod editor_config;
type LanguageMapping = HashMap<String, Language>;
pub use crate::config::config::*;
pub use crate::config::directories::*;
pub use crate::config::editor_config::*;
#[derive(Debug, Clone)]
pub struct EditorConfig {
character_size: u16,
font_path: String,
current_theme: String,
margin_left: u16,
margin_top: u16,
}
pub type ConfigAccess = Arc<RwLock<Config>>;
impl EditorConfig {
pub fn new() -> Self {
let mut default_font_path = directories::fonts_dir();
default_font_path.push("DejaVuSansMono.ttf");
Self {
character_size: 14,
font_path: default_font_path.to_str().unwrap().to_string(),
current_theme: "railscasts".to_string(),
margin_left: 10,
margin_top: 10,
}
}
pub fn character_size(&self) -> u16 {
self.character_size
}
pub fn font_path(&self) -> &String {
&self.font_path
}
pub fn current_theme(&self) -> &String {
&self.current_theme
}
pub fn margin_left(&self) -> u16 {
self.margin_left
}
pub fn margin_top(&self) -> u16 {
self.margin_top
}
}
#[derive(Debug, Clone)]
pub struct Config {
width: u32,
height: u32,
scroll_speed: i32,
menu_height: u16,
editor_config: EditorConfig,
theme: Theme,
extensions_mapping: LanguageMapping,
}
impl Config {
pub fn new() -> Self {
creator::create();
let editor_config = EditorConfig::new();
let mut extensions_mapping = HashMap::new();
extensions_mapping.insert(".".to_string(), Language::PlainText);
extensions_mapping.insert("txt".to_string(), Language::PlainText);
extensions_mapping.insert("rs".to_string(), Language::Rust);
Self {
width: 1024,
height: 860,
scroll_speed: 10,
menu_height: 60,
theme: Theme::load(editor_config.current_theme().clone()),
editor_config,
extensions_mapping,
}
}
pub fn scroll_speed(&self) -> i32 {
self.scroll_speed
}
pub fn width(&self) -> u32 {
self.width
}
pub fn set_width(&mut self, w: u32) {
self.width = w;
}
pub fn height(&self) -> u32 {
self.height
}
pub fn set_height(&mut self, h: u32) {
self.height = h;
}
pub fn editor_config(&self) -> &EditorConfig {
&self.editor_config
}
pub fn theme(&self) -> &Theme {
&self.theme
}
pub fn menu_height(&self) -> u16 {
self.menu_height
}
pub fn editor_top_margin(&self) -> i32 {
(self.menu_height() as i32) + (self.editor_config().margin_top() as i32)
}
pub fn editor_left_margin(&self) -> i32 {
self.editor_config().margin_left() as i32
}
pub fn extensions_mapping(&self) -> &LanguageMapping {
&self.extensions_mapping
}
pub trait ConfigHolder {
fn config(&self) -> &ConfigAccess;
}

View File

@ -3,18 +3,17 @@ use std::ops::Deref;
pub mod plain;
pub mod rust_lang;
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Ord, Eq)]
pub enum Language {
PlainText,
Rust,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub enum TokenType {
Whitespace { token: Token },
Keyword { token: Token },
String { token: Token },
Number { token: Token },
Identifier { token: Token },
Literal { token: Token },
Comment { token: Token },
@ -34,9 +33,6 @@ impl TokenType {
TokenType::String { token } => TokenType::String {
token: token.move_to(line, character, start, end),
},
TokenType::Number { token } => TokenType::Number {
token: token.move_to(line, character, start, end),
},
TokenType::Identifier { token } => TokenType::Identifier {
token: token.move_to(line, character, start, end),
},
@ -78,7 +74,6 @@ impl Deref for TokenType {
TokenType::Whitespace { token } => token,
TokenType::Keyword { token } => token,
TokenType::String { token } => token,
TokenType::Number { token } => token,
TokenType::Identifier { token } => token,
TokenType::Literal { token } => token,
TokenType::Comment { token } => token,
@ -88,7 +83,13 @@ impl Deref for TokenType {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Span {
pub lo: usize,
pub hi: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Token {
line: usize,
character: usize,
@ -97,12 +98,6 @@ pub struct Token {
text: String,
}
#[derive(Debug, Clone, Copy)]
pub struct Span {
pub lo: usize,
pub hi: usize,
}
impl Token {
pub fn new(text: String, line: usize, character: usize, start: usize, end: usize) -> Self {
Self {
@ -157,3 +152,73 @@ pub fn parse(text: String, language: &Language) -> Vec<TokenType> {
.collect(),
}
}
#[cfg(test)]
mod tests {
use crate::lexer::*;
#[test]
fn must_parse_plain() {
let buffer = "foo bar";
let language = &Language::PlainText;
let result = parse(buffer.to_string(), language);
assert_eq!(result.len(), 3);
}
#[test]
fn must_parse_rust() {
let buffer = "foo bar";
let language = &Language::Rust;
let result = parse(buffer.to_string(), language);
assert_eq!(result.len(), 3);
}
#[test]
fn must_return_valid_value_for_text() {
let token = Token::new("a".to_string(), 1, 2, 3, 4);
let result = token.text();
let text: String = "a".to_string();
let expected = &text;
assert_eq!(result, expected);
}
#[test]
fn must_return_valid_value_for_line() {
let token = Token::new("a".to_string(), 1, 2, 3, 4);
let result = token.line();
let expected = 1;
assert_eq!(result, expected);
}
#[test]
fn must_return_valid_value_for_character() {
let token = Token::new("a".to_string(), 1, 2, 3, 4);
let result = token.character();
let expected = 2;
assert_eq!(result, expected);
}
#[test]
fn must_return_valid_value_for_start() {
let token = Token::new("a".to_string(), 1, 2, 3, 4);
let result = token.start();
let expected = 3;
assert_eq!(result, expected);
}
#[test]
fn must_return_valid_value_for_end() {
let token = Token::new("a".to_string(), 1, 2, 3, 4);
let result = token.end();
let expected = 4;
assert_eq!(result, expected);
}
#[test]
fn must_return_valid_value_for_move_to() {
let token = Token::new("a".to_string(), 1, 2, 3, 4);
let result = token.move_to(5, 6, 7, 8);
let expected = Token::new("a".to_string(), 5, 6, 7, 8);
assert_eq!(result, expected);
}
}

View File

@ -77,3 +77,140 @@ pub mod lexer {
}
}
}
#[cfg(test)]
mod tests {
use crate::lexer::plain::*;
use crate::lexer::*;
#[test]
fn must_parse_simple_text() {
let code = "foo";
let lexer = lexer::Lexer::new(code);
let result: Vec<TokenType> = lexer.map(|pair| pair.0).collect();
let expected: Vec<TokenType> = vec![TokenType::Identifier {
token: Token::new("foo".to_string(), 0, 0, 0, 3),
}];
assert_eq!(result, expected);
}
#[test]
fn must_parse_long_text() {
let code = "foobarhelloworldexamplecomtesttest";
let lexer = lexer::Lexer::new(code);
let result: Vec<TokenType> = lexer.map(|pair| pair.0).collect();
let expected: Vec<TokenType> = vec![TokenType::Identifier {
token: Token::new(
"foobarhelloworldexamplecomtesttest".to_string(),
0,
0,
0,
34,
),
}];
assert_eq!(result, expected);
}
#[test]
fn must_parse_text_with_space() {
let code = "foo bar";
let lexer = lexer::Lexer::new(code);
let result: Vec<TokenType> = lexer.map(|pair| pair.0).collect();
let expected: Vec<TokenType> = vec![
TokenType::Identifier {
token: Token::new("foo".to_string(), 0, 0, 0, 3),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 3, 3, 4),
},
TokenType::Identifier {
token: Token::new("bar".to_string(), 0, 4, 4, 7),
},
];
assert_eq!(result, expected);
}
#[test]
fn must_parse_text_with_one_multi_character_space() {
let code = "foo bar";
let lexer = lexer::Lexer::new(code);
let result: Vec<TokenType> = lexer.map(|pair| pair.0).collect();
let expected: Vec<TokenType> = vec![
TokenType::Identifier {
token: Token::new("foo".to_string(), 0, 0, 0, 3),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 3, 3, 4),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 4, 4, 5),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 5, 5, 6),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 6, 6, 7),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 7, 7, 8),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 8, 8, 9),
},
TokenType::Identifier {
token: Token::new("bar".to_string(), 0, 9, 9, 12),
},
];
assert_eq!(result, expected);
}
#[test]
fn must_parse_text_with_multiple_spaces() {
let code = "foo bar hello world";
let lexer = lexer::Lexer::new(code);
let result: Vec<TokenType> = lexer.map(|pair| pair.0).collect();
let expected: Vec<TokenType> = vec![
TokenType::Identifier {
token: Token::new("foo".to_string(), 0, 0, 0, 3),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 3, 3, 4),
},
TokenType::Identifier {
token: Token::new("bar".to_string(), 0, 4, 4, 7),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 7, 7, 8),
},
TokenType::Identifier {
token: Token::new("hello".to_string(), 0, 8, 8, 13),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 13, 13, 14),
},
TokenType::Identifier {
token: Token::new("world".to_string(), 0, 14, 14, 19),
},
];
assert_eq!(result, expected);
}
#[test]
fn must_parse_text_with_new_line() {
let code = "foo\nbar";
let lexer = lexer::Lexer::new(code);
let result: Vec<TokenType> = lexer.map(|pair| pair.0).collect();
let expected: Vec<TokenType> = vec![
TokenType::Identifier {
token: Token::new("foo".to_string(), 0, 0, 0, 3),
},
TokenType::Whitespace {
token: Token::new("\n".to_string(), 1, 0, 3, 4),
},
TokenType::Identifier {
token: Token::new("bar".to_string(), 1, 1, 4, 7),
},
];
assert_eq!(result, expected);
}
}

View File

@ -11,23 +11,23 @@ pub mod lexer {
token: Token::new(text.to_string(), 0, 0, 0, 0)
}, text),
r"(\d+|\d+\.\d+|'[\S]')" => (TokenType::Literal {
r"([0-9]+|[0-9]+\.[0-9]+|'[^']')" => (TokenType::Literal {
token: Token::new(text.to_string(), 0, 0, 0, 0)
}, text),
r"[+-/*%=<>]" => (TokenType::Operator {
r"(->|[+-/*%=<>#])" => (TokenType::Operator {
token: Token::new(text.to_string(), 0, 0, 0, 0)
}, text),
r"(:|::|\{|\}|\[|\]|,)" => (TokenType::Separator {
r"(:|::|\{|\}|\[|\]|;|,|\)|\()" => (TokenType::Separator {
token: Token::new(text.to_string(), 0, 0, 0, 0)
}, text),
r"(let|fn|type|struct|pub|impl|for|self|Self|mod|use|enum)" => (TokenType::Keyword {
r"(let|fn|type|struct|pub|impl|for|self|Self|mod|use|enum|(iu)(8|16|32)|usize|bool)" => (TokenType::Keyword {
token: Token::new(text.to_string(), 0, 0, 0, 0)
}, text),
r"[^ \t\r\n:+-/*,]+" => (TokenType::Identifier {
r"[^0-9 \t\r\n:+-/*,';<>=%()\[\]{}][^ \t\r\n:+-/*,';<>=%()\[\]{}]*" => (TokenType::Identifier {
token: Token::new(text.to_string(), 0, 0, 0, 0)
}, text),
}
@ -94,3 +94,383 @@ pub mod lexer {
}
}
}
#[cfg(test)]
mod tests {
use crate::lexer::rust_lang::*;
use crate::lexer::*;
#[test]
fn must_parse_simple_text() {
let code = "foo";
let lexer = lexer::Lexer::new(code);
let result: Vec<TokenType> = lexer.map(|pair| pair.0).collect();
let expected: Vec<TokenType> = vec![TokenType::Identifier {
token: Token::new("foo".to_string(), 0, 0, 0, 3),
}];
assert_eq!(result, expected);
}
#[test]
fn must_parse_long_text() {
let code = "foobarhelloworldexamplecomtesttest";
let lexer = lexer::Lexer::new(code);
let result: Vec<TokenType> = lexer.map(|pair| pair.0).collect();
let expected: Vec<TokenType> = vec![TokenType::Identifier {
token: Token::new(
"foobarhelloworldexamplecomtesttest".to_string(),
0,
0,
0,
34,
),
}];
assert_eq!(result, expected);
}
#[test]
fn must_parse_text_with_space() {
let code = "foo bar";
let lexer = lexer::Lexer::new(code);
let result: Vec<TokenType> = lexer.map(|pair| pair.0).collect();
let expected: Vec<TokenType> = vec![
TokenType::Identifier {
token: Token::new("foo".to_string(), 0, 0, 0, 3),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 3, 3, 4),
},
TokenType::Identifier {
token: Token::new("bar".to_string(), 0, 4, 4, 7),
},
];
assert_eq!(result, expected);
}
#[test]
fn must_parse_text_with_one_multi_character_space() {
let code = "foo bar";
let lexer = lexer::Lexer::new(code);
let result: Vec<TokenType> = lexer.map(|pair| pair.0).collect();
let expected: Vec<TokenType> = vec![
TokenType::Identifier {
token: Token::new("foo".to_string(), 0, 0, 0, 3),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 3, 3, 9),
},
TokenType::Identifier {
token: Token::new("bar".to_string(), 0, 9, 9, 12),
},
];
assert_eq!(result, expected);
}
#[test]
fn must_parse_text_with_multiple_spaces() {
let code = "foo bar hello world";
let lexer = lexer::Lexer::new(code);
let result: Vec<TokenType> = lexer.map(|pair| pair.0).collect();
let expected: Vec<TokenType> = vec![
TokenType::Identifier {
token: Token::new("foo".to_string(), 0, 0, 0, 3),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 3, 3, 4),
},
TokenType::Identifier {
token: Token::new("bar".to_string(), 0, 4, 4, 7),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 7, 7, 8),
},
TokenType::Identifier {
token: Token::new("hello".to_string(), 0, 8, 8, 13),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 13, 13, 14),
},
TokenType::Identifier {
token: Token::new("world".to_string(), 0, 14, 14, 19),
},
];
assert_eq!(result, expected);
}
#[test]
fn must_parse_text_with_new_line() {
let code = "foo\nbar";
let lexer = lexer::Lexer::new(code);
let result: Vec<TokenType> = lexer.map(|pair| pair.0).collect();
let expected: Vec<TokenType> = vec![
TokenType::Identifier {
token: Token::new("foo".to_string(), 0, 0, 0, 3),
},
TokenType::Whitespace {
token: Token::new("\n".to_string(), 1, 0, 3, 4),
},
TokenType::Identifier {
token: Token::new("bar".to_string(), 1, 1, 4, 7),
},
];
assert_eq!(result, expected);
}
#[test]
fn must_parse_keyword_let_and_char_literal() {
let code = "let a = 'b';";
let lexer = lexer::Lexer::new(code);
let result: Vec<TokenType> = lexer.map(|pair| pair.0).collect();
let expected: Vec<TokenType> = vec![
TokenType::Keyword {
token: Token::new("let".to_string(), 0, 0, 0, 3),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 3, 3, 4),
},
TokenType::Identifier {
token: Token::new("a".to_string(), 0, 4, 4, 5),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 5, 5, 6),
},
TokenType::Operator {
token: Token::new("=".to_string(), 0, 6, 6, 7),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 7, 7, 8),
},
TokenType::Literal {
token: Token::new("'b'".to_string(), 0, 8, 8, 11),
},
TokenType::Separator {
token: Token::new(";".to_string(), 0, 11, 11, 12),
},
];
assert_eq!(result, expected);
}
#[test]
fn must_parse_keyword_let_and_number_literal() {
let code = "let a = 684;";
let lexer = lexer::Lexer::new(code);
let result: Vec<TokenType> = lexer.map(|pair| pair.0).collect();
let expected: Vec<TokenType> = vec![
TokenType::Keyword {
token: Token::new("let".to_string(), 0, 0, 0, 3),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 3, 3, 4),
},
TokenType::Identifier {
token: Token::new("a".to_string(), 0, 4, 4, 5),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 5, 5, 6),
},
TokenType::Operator {
token: Token::new("=".to_string(), 0, 6, 6, 7),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 7, 7, 8),
},
TokenType::Literal {
token: Token::new("684".to_string(), 0, 8, 8, 11),
},
TokenType::Separator {
token: Token::new(";".to_string(), 0, 11, 11, 12),
},
];
assert_eq!(result, expected);
}
#[test]
fn must_parse_calculation() {
let code = "let a = b + 684;";
let lexer = lexer::Lexer::new(code);
let result: Vec<TokenType> = lexer.map(|pair| pair.0).collect();
let expected: Vec<TokenType> = vec![
TokenType::Keyword {
token: Token::new("let".to_string(), 0, 0, 0, 3),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 3, 3, 4),
},
TokenType::Identifier {
token: Token::new("a".to_string(), 0, 4, 4, 5),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 5, 5, 6),
},
TokenType::Operator {
token: Token::new("=".to_string(), 0, 6, 6, 7),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 7, 7, 8),
},
TokenType::Identifier {
token: Token::new("b".to_string(), 0, 8, 8, 9),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 9, 9, 10),
},
TokenType::Operator {
token: Token::new("+".to_string(), 0, 10, 10, 11),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 11, 11, 12),
},
TokenType::Literal {
token: Token::new("684".to_string(), 0, 12, 12, 15),
},
TokenType::Separator {
token: Token::new(";".to_string(), 0, 15, 15, 16),
},
];
assert_eq!(result, expected);
}
#[test]
fn must_parse_function() {
let code = r"fn foo(a: i32, b: i32) -> int {
a % b
}";
let lexer = lexer::Lexer::new(code);
let result: Vec<TokenType> = lexer.map(|pair| pair.0).collect();
let expected: Vec<TokenType> = vec![
TokenType::Keyword {
token: Token::new("fn".to_string(), 0, 0, 0, 2),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 2, 2, 3),
},
TokenType::Identifier {
token: Token::new("foo".to_string(), 0, 3, 3, 6),
},
TokenType::Separator {
token: Token::new("(".to_string(), 0, 6, 6, 7),
},
TokenType::Identifier {
token: Token::new("a".to_string(), 0, 7, 7, 8),
},
TokenType::Separator {
token: Token::new(":".to_string(), 0, 8, 8, 9),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 9, 9, 10),
},
TokenType::Identifier {
token: Token::new("i32".to_string(), 0, 10, 10, 13),
},
TokenType::Operator {
token: Token::new(",".to_string(), 0, 13, 13, 14),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 14, 14, 15),
},
TokenType::Identifier {
token: Token::new("b".to_string(), 0, 15, 15, 16),
},
TokenType::Separator {
token: Token::new(":".to_string(), 0, 16, 16, 17),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 17, 17, 18),
},
TokenType::Identifier {
token: Token::new("i32".to_string(), 0, 18, 18, 21),
},
TokenType::Separator {
token: Token::new(")".to_string(), 0, 21, 21, 22),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 22, 22, 23),
},
TokenType::Operator {
token: Token::new("->".to_string(), 0, 23, 23, 25),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 25, 25, 26),
},
TokenType::Identifier {
token: Token::new("int".to_string(), 0, 26, 26, 29),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 29, 29, 30),
},
TokenType::Separator {
token: Token::new("{".to_string(), 0, 30, 30, 31),
},
TokenType::Whitespace {
token: Token::new("\n".to_string(), 1, 0, 31, 32),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 1, 1, 32, 44),
},
TokenType::Identifier {
token: Token::new("a".to_string(), 1, 13, 44, 45),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 1, 14, 45, 46),
},
TokenType::Operator {
token: Token::new("%".to_string(), 1, 15, 46, 47),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 1, 16, 47, 48),
},
TokenType::Identifier {
token: Token::new("b".to_string(), 1, 17, 48, 49),
},
TokenType::Whitespace {
token: Token::new("\n".to_string(), 2, 0, 49, 50),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 2, 1, 50, 58),
},
TokenType::Separator {
token: Token::new("}".to_string(), 2, 9, 58, 59),
},
];
assert_eq!(result, expected);
}
#[test]
fn must_parse_derive() {
let buffer = "#[derive(Debug, Clone)]";
let result: Vec<TokenType> = lexer::Lexer::new(buffer).map(|p| p.0).collect();
let expected: Vec<TokenType> = vec![
TokenType::Operator {
token: Token::new("#".to_string(), 0, 0, 0, 1),
},
TokenType::Separator {
token: Token::new("[".to_string(), 0, 1, 1, 2),
},
TokenType::Identifier {
token: Token::new("derive".to_string(), 0, 2, 2, 8),
},
TokenType::Separator {
token: Token::new("(".to_string(), 0, 8, 8, 9),
},
TokenType::Identifier {
token: Token::new("Debug".to_string(), 0, 9, 9, 14),
},
TokenType::Operator {
token: Token::new(",".to_string(), 0, 14, 14, 15),
},
TokenType::Whitespace {
token: Token::new(" ".to_string(), 0, 15, 15, 16),
},
TokenType::Identifier {
token: Token::new("Clone".to_string(), 0, 16, 16, 21),
},
TokenType::Separator {
token: Token::new(")".to_string(), 0, 21, 21, 22),
},
TokenType::Separator {
token: Token::new("]".to_string(), 0, 22, 22, 23),
},
];
assert_eq!(result, expected);
}
}

View File

@ -13,6 +13,8 @@ extern crate serde_json;
#[macro_use]
extern crate log;
extern crate simplelog;
#[macro_use]
extern crate lazy_static;
use crate::app::Application;
use crate::config::directories::log_dir;
@ -25,22 +27,27 @@ pub mod app;
pub mod config;
pub mod lexer;
pub mod renderer;
#[cfg(test)]
pub mod tests;
pub mod themes;
pub mod ui;
fn init_logger() {
use simplelog::SharedLogger;
let mut log_file_path = log_dir();
log_file_path.push("rider.log");
CombinedLogger::init(vec![
TermLogger::new(LevelFilter::Warn, Config::default()).unwrap(),
WriteLogger::new(
let mut outputs: Vec<Box<SharedLogger>> = vec![WriteLogger::new(
LevelFilter::Info,
Config::default(),
File::create(log_file_path).unwrap(),
),
])
.unwrap();
)];
if let Some(term) = TermLogger::new(LevelFilter::Warn, Config::default()) {
outputs.push(term);
}
CombinedLogger::init(outputs).unwrap();
}
fn main() {

View File

@ -2,6 +2,7 @@ use sdl2::image::LoadTexture;
use sdl2::pixels::Color;
use sdl2::render::{Texture, TextureCreator};
use sdl2::ttf::{Font, Sdl2TtfContext};
use sdl2::video::WindowContext as WinCtxt;
use std::borrow::Borrow;
use std::collections::HashMap;
#[allow(unused_imports)]
@ -9,6 +10,10 @@ use std::env;
use std::hash::Hash;
use std::rc::Rc;
//noinspection RsWrongLifetimeParametersNumber
pub type RcTex<'l> = Rc<Texture<'l>>;
pub type RcFont<'l> = Rc<Font<'l, 'static>>;
pub trait ResourceLoader<'l, R> {
type Args: ?Sized;
@ -70,6 +75,12 @@ impl<'a> From<&'a FontDetails> for FontDetails {
pub type TextureManager<'l, T> = ResourceManager<'l, String, Texture<'l>, TextureCreator<T>>;
pub type FontManager<'l> = ResourceManager<'l, FontDetails, Font<'l, 'static>, Sdl2TtfContext>;
pub trait ManagersHolder<'l> {
fn font_manager(&mut self) -> &mut FontManager<'l>;
fn texture_manager(&mut self) -> &mut TextureManager<'l, WinCtxt>;
}
#[derive(Clone)]
pub struct ResourceManager<'l, K, R, L>
where
@ -132,9 +143,18 @@ impl<'l> ResourceLoader<'l, Font<'l, 'static>> for Sdl2TtfContext {
}
}
impl<'l, T> TextureManager<'l, T> {
pub trait TextTextureManager<'l> {
//noinspection RsWrongLifetimeParametersNumber
pub fn load_text(
fn load_text(
&mut self,
details: &mut TextDetails,
font: &Rc<Font>,
) -> Result<Rc<Texture<'l>>, String>;
}
impl<'l, T> TextTextureManager<'l> for TextureManager<'l, T> {
//noinspection RsWrongLifetimeParametersNumber
fn load_text(
&mut self,
details: &mut TextDetails,
font: &Rc<Font>,

View File

@ -1,90 +1,5 @@
use crate::app::WindowCanvas;
use crate::config::Config;
use crate::renderer::managers::TextDetails;
use crate::renderer::managers::{FontManager, TextureManager};
use sdl2::rect::{Point, Rect};
use sdl2::render::{Texture, TextureCreator};
use sdl2::ttf::Sdl2TtfContext;
use sdl2::video::WindowContext;
use std::rc::Rc;
use std::sync::*;
pub mod managers;
pub mod renderer;
pub struct Renderer<'a> {
config: Arc<RwLock<Config>>,
font_manager: FontManager<'a>,
texture_manager: TextureManager<'a, WindowContext>,
scroll: Point,
}
impl<'a> Renderer<'a> {
pub fn new(
config: Arc<RwLock<Config>>,
font_context: &'a Sdl2TtfContext,
texture_creator: &'a TextureCreator<WindowContext>,
) -> Self {
Self {
config,
font_manager: FontManager::new(&font_context),
texture_manager: TextureManager::new(&texture_creator),
scroll: (0, 0).into(),
}
}
pub fn config(&self) -> &Arc<RwLock<Config>> {
&self.config
}
pub fn font_manager(&mut self) -> &mut FontManager<'a> {
&mut self.font_manager
}
pub fn texture_manager(&mut self) -> &mut TextureManager<'a, WindowContext> {
&mut self.texture_manager
}
pub fn scroll(&self) -> &Point {
&self.scroll
}
pub fn render_texture(
&mut self,
canvas: &mut WindowCanvas,
texture: &Rc<Texture>,
src: &Rect,
dest: &Rect,
) {
canvas
.copy_ex(
texture,
Some(src.clone()),
Some(dest.clone()),
0.0,
None,
false,
false,
)
.unwrap();
}
pub fn render_text(&mut self, details: TextDetails) -> Option<Rc<Texture>> {
let font = self.font_manager.load(&details.font).unwrap();
let surface = font.render(details.text.as_str()).blended(details.color);
let surface = if let Ok(s) = surface {
s
} else {
return None;
};
let texture = self
.texture_manager
.loader()
.create_texture_from_surface(&surface);
let texture = if let Ok(t) = texture {
Rc::new(t)
} else {
return None;
};
Some(texture)
}
}
pub use crate::renderer::managers::*;
pub use crate::renderer::renderer::*;

45
src/renderer/renderer.rs Normal file
View File

@ -0,0 +1,45 @@
use crate::app::WindowCanvas as WC;
use crate::config::{Config, ConfigAccess, ConfigHolder};
use crate::renderer::managers::*;
use sdl2::rect::{Point, Rect};
use sdl2::render::{Texture, TextureCreator};
use sdl2::ttf::Sdl2TtfContext;
use sdl2::video::WindowContext as WinCtxt;
use std::rc::Rc;
use std::sync::*;
pub struct Renderer<'l> {
config: ConfigAccess,
font_manager: FontManager<'l>,
texture_manager: TextureManager<'l, WinCtxt>,
}
impl<'l> Renderer<'l> {
pub fn new(
config: ConfigAccess,
font_context: &'l Sdl2TtfContext,
texture_creator: &'l TextureCreator<WinCtxt>,
) -> Self {
Self {
config,
font_manager: FontManager::new(&font_context),
texture_manager: TextureManager::new(&texture_creator),
}
}
}
impl<'l> ManagersHolder<'l> for Renderer<'l> {
fn font_manager(&mut self) -> &mut FontManager<'l> {
&mut self.font_manager
}
fn texture_manager(&mut self) -> &mut TextureManager<'l, WinCtxt> {
&mut self.texture_manager
}
}
impl<'l> ConfigHolder for Renderer<'l> {
fn config(&self) -> &ConfigAccess {
&self.config
}
}

28
src/tests.rs Normal file
View File

@ -0,0 +1,28 @@
#[cfg(test)]
pub mod support {
use crate::config::*;
use crate::renderer::*;
use sdl2::render::{Canvas, WindowCanvas};
use sdl2::*;
use sdl2::{Sdl, TimerSubsystem, VideoSubsystem};
use std::borrow::*;
use std::sync::*;
pub fn build_config() -> Arc<RwLock<Config>> {
Arc::new(RwLock::new(Config::new()))
}
pub fn build_canvas() -> WindowCanvas {
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
let window = video_subsystem
.window("Test", 1, 1)
.borderless()
.opengl()
.build()
.unwrap();
window.into_canvas().accelerated().build().unwrap()
}
}

View File

@ -1,248 +0,0 @@
use crate::app::{UpdateResult as UR, WindowCanvas as WC};
use crate::config::Config;
use crate::renderer::Renderer;
use crate::ui::text_character::TextCharacter;
use crate::ui::*;
use sdl2::pixels::Color;
use sdl2::rect::{Point, Rect};
use sdl2::render::Texture;
use std::ops::Deref;
use std::rc::Rc;
use std::sync::*;
#[derive(Clone, Debug, PartialEq)]
pub enum CaretState {
Bright,
Blur,
}
#[derive(Clone, Debug, PartialEq)]
pub enum MoveDirection {
Left,
Right,
Up,
Down,
}
//#[derive(Clone, Debug, PartialEq)]
//pub enum CaretLocation {
// FirstLineFirstCharacter,
// FirstLine(usize), // with character location
// LastLineFirstCharacter,
// LastLine(usize), // with character location
// FirstCharacter(usize),// with line number
// LastCharacter(usize), // with line number
// Other(usize, usize), // with line number and character number
//}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CaretPosition {
text_position: usize,
line_number: usize,
line_position: usize,
}
impl CaretPosition {
pub fn new(text_position: usize, line_number: usize, line_position: usize) -> Self {
Self {
text_position,
line_number,
line_position,
}
}
pub fn text_position(&self) -> usize {
self.text_position.clone()
}
pub fn line_number(&self) -> usize {
self.line_number.clone()
}
pub fn line_position(&self) -> usize {
self.line_position.clone()
}
pub fn reset(&mut self) {
self.text_position = 0;
self.line_number = 0;
self.line_position = 0;
}
pub fn set_text_position(&mut self, n: usize) {
self.text_position = n;
}
pub fn set_line_number(&mut self, n: usize) {
self.line_number = n;
}
pub fn set_line_position(&mut self, n: usize) {
self.line_position = n;
}
pub fn moved(&self, text_position: i32, line_number: i32, line_position: i32) -> Self {
Self {
text_position: (self.text_position as i32 + text_position) as usize,
line_number: (self.line_number as i32 + line_number) as usize,
line_position: (self.line_position as i32 + line_position) as usize,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CaretRenderPosition {
dest: Rect,
reset_position: Rect,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CaretColor {
bright: Color,
blur: Color,
}
impl CaretColor {
pub fn new(bright: Color, blur: Color) -> Self {
Self { bright, blur }
}
pub fn bright(&self) -> &Color {
&self.bright
}
pub fn blur(&self) -> &Color {
&self.blur
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Caret {
pending: bool,
blink_delay: u8,
state: CaretState,
position: CaretPosition,
dest: Rect,
colors: CaretColor,
}
impl Caret {
pub fn new(config: Arc<RwLock<Config>>) -> Self {
let read_config = config.read().unwrap();
let bright = read_config.theme().caret().bright().color().into();
let blur = read_config.theme().caret().blur().color().into();
Self {
state: CaretState::Bright,
blink_delay: 0,
dest: Rect::new(0, 0, 6, 0),
colors: CaretColor { bright, blur },
pending: true,
position: CaretPosition {
text_position: 0,
line_number: 0,
line_position: 0,
},
}
}
fn toggle_state(&mut self) {
self.state = if self.state == CaretState::Bright {
CaretState::Blur
} else {
CaretState::Bright
};
}
pub fn reset_caret(&mut self) {
self.dest.set_x(0);
self.dest.set_y(0);
self.position.reset();
}
pub fn move_caret(&mut self, position: CaretPosition, pos: Point) {
self.position = position;
self.dest.set_x(pos.x());
self.dest.set_y(pos.y());
}
pub fn dest(&self) -> &Rect {
&self.dest
}
pub fn position(&self) -> &CaretPosition {
&self.position
}
}
impl Deref for Caret {
type Target = CaretPosition;
fn deref(&self) -> &<Self as Deref>::Target {
self.position()
}
}
impl Render for Caret {
fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, parent: Parent) -> UR {
let dest = match parent {
Some(parent) => move_render_point(parent.render_start_point(), self.dest()),
None => self.dest().clone(),
};
let start = Point::new(dest.x(), dest.y());
let end = Point::new(dest.x(), dest.y() + dest.height() as i32);
let color = match self.state {
CaretState::Bright => self.colors.bright(),
CaretState::Blur => self.colors.blur(),
}
.clone();
canvas.set_draw_color(color);
canvas
.draw_line(start, end)
.unwrap_or_else(|_| panic!("Failed to draw a caret"));
UR::NoOp
}
fn prepare_ui(&mut self, renderer: &mut Renderer) {
if !self.pending {
return;
}
if let Some(rect) = get_text_character_rect('W', renderer) {
self.dest.set_height(rect.height());
}
self.pending = false;
}
}
impl Update for Caret {
fn update(&mut self, _ticks: i32, _context: &UpdateContext) -> UR {
self.blink_delay += 1;
if self.blink_delay >= 30 {
self.blink_delay = 0;
self.toggle_state();
}
UR::NoOp
}
}
impl ClickHandler for Caret {
fn on_left_click(&mut self, _point: &Point, _context: &UpdateContext) -> UR {
UR::NoOp
}
fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool {
is_in_rect(
point,
&match context {
&UpdateContext::ParentPosition(p) => move_render_point(p, self.dest()),
_ => self.dest().clone(),
},
)
}
}
impl RenderBox for Caret {
fn render_start_point(&self) -> Point {
self.dest().top_left()
}
}

322
src/ui/caret/caret.rs Normal file
View File

@ -0,0 +1,322 @@
use crate::app::{UpdateResult as UR, WindowCanvas as WC};
use crate::config::*;
use crate::renderer::*;
use crate::ui::text_character::TextCharacter;
use crate::ui::*;
use sdl2::pixels::Color;
use sdl2::rect::{Point, Rect};
use sdl2::render::Texture;
use std::ops::Deref;
use std::sync::*;
#[derive(Clone, Debug, PartialEq)]
pub struct Caret {
pending: bool,
blink_delay: u8,
state: CaretState,
position: CaretPosition,
dest: Rect,
colors: CaretColor,
}
impl Caret {
pub fn new(config: ConfigAccess) -> Self {
let read_config = config.read().unwrap();
let bright = read_config.theme().caret().bright().color().into();
let blur = read_config.theme().caret().blur().color().into();
Self {
state: CaretState::Bright,
blink_delay: 0,
dest: Rect::new(0, 0, 6, 0),
colors: CaretColor::new(bright, blur),
pending: true,
position: CaretPosition::new(0, 0, 0),
}
}
fn toggle_state(&mut self) {
self.state = match self.state {
CaretState::Bright => CaretState::Blur,
CaretState::Blur => CaretState::Bright,
};
}
pub fn reset_caret(&mut self) {
self.dest.set_x(0);
self.dest.set_y(0);
self.position.reset();
}
pub fn move_caret(&mut self, position: CaretPosition, pos: Point) {
self.position = position;
self.dest.set_x(pos.x());
self.dest.set_y(pos.y());
}
pub fn position(&self) -> &CaretPosition {
&self.position
}
pub fn state(&self) -> &CaretState {
&self.state
}
}
impl Deref for Caret {
type Target = CaretPosition;
fn deref(&self) -> &<Self as Deref>::Target {
self.position()
}
}
impl Render for Caret {
fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, parent: Parent) {
let dest = match parent {
Some(parent) => move_render_point(parent.render_start_point(), self.dest()),
None => self.dest().clone(),
};
let start = Point::new(dest.x(), dest.y());
let end = Point::new(dest.x(), dest.y() + dest.height() as i32);
let color = match self.state {
CaretState::Bright => self.colors.bright(),
CaretState::Blur => self.colors.blur(),
}
.clone();
canvas.set_draw_color(color);
canvas
.draw_line(start, end)
.unwrap_or_else(|_| panic!("Failed to draw a caret"));
}
fn prepare_ui(&mut self, renderer: &mut Renderer) {
if !self.pending {
return;
}
if let Some(rect) = get_text_character_rect('W', renderer) {
self.dest.set_height(rect.height());
}
self.pending = false;
}
}
impl Update for Caret {
fn update(&mut self, _ticks: i32, _context: &UpdateContext) -> UR {
self.blink_delay += 1;
if self.blink_delay >= 30 {
self.blink_delay = 0;
self.toggle_state();
}
UR::NoOp
}
}
impl ClickHandler for Caret {
fn on_left_click(&mut self, _point: &Point, _context: &UpdateContext) -> UR {
UR::NoOp
}
fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool {
is_in_rect(
point,
&match context {
&UpdateContext::ParentPosition(p) => move_render_point(p, self.dest()),
_ => self.dest().clone(),
},
)
}
}
impl RenderBox for Caret {
fn render_start_point(&self) -> Point {
self.dest().top_left()
}
fn dest(&self) -> &Rect {
&self.dest
}
}
#[cfg(test)]
mod test_own_methods {
use crate::renderer::*;
use crate::tests::*;
use crate::ui::*;
use sdl2::pixels::*;
use sdl2::rect::*;
use std::sync::*;
#[test]
fn assert_move_caret() {
let config = support::build_config();
let mut widget = Caret::new(Arc::clone(&config));
widget.move_caret(widget.moved(10, 21, 34), Point::new(10, 20));
let result = (
widget.text_position(),
widget.line_number(),
widget.line_position(),
widget.dest().clone(),
);
let expected = (10, 21, 34, Rect::new(10, 20, 6, 1));
assert_eq!(result, expected);
}
#[test]
fn assert_reset() {
let config = support::build_config();
let mut widget = Caret::new(Arc::clone(&config));
widget.reset_caret();
let result = (
widget.text_position(),
widget.line_number(),
widget.line_position(),
widget.dest().clone(),
);
let expected = (0, 0, 0, Rect::new(0, 0, 6, 1));
assert_eq!(result, expected);
}
#[test]
fn assert_toggle_state() {
let config = support::build_config();
let mut widget = Caret::new(Arc::clone(&config));
let old = widget.state().clone();
widget.toggle_state();
let new = widget.state().clone();
assert_ne!(old, new);
let old = widget.state().clone();
widget.toggle_state();
let new = widget.state().clone();
assert_ne!(old, new);
let old = widget.state().clone();
widget.toggle_state();
let new = widget.state().clone();
assert_ne!(old, new);
}
}
#[cfg(test)]
mod test_deref {
use crate::renderer::*;
use crate::tests::*;
use crate::ui::*;
use sdl2::pixels::*;
use sdl2::rect::*;
use std::sync::*;
#[test]
fn must_deref_text_position() {
let config = support::build_config();
let mut widget = Caret::new(Arc::clone(&config));
widget.move_caret(widget.moved(10, 21, 34), Point::new(0, 0));
let result = widget.text_position();
let expected: usize = 10;
assert_eq!(result, expected);
}
#[test]
fn must_deref_line_number() {
let config = support::build_config();
let mut widget = Caret::new(Arc::clone(&config));
widget.move_caret(widget.moved(10, 21, 34), Point::new(0, 0));
let result = widget.line_number();
let expected: usize = 21;
assert_eq!(result, expected);
}
#[test]
fn must_deref_line_position() {
let config = support::build_config();
let mut widget = Caret::new(Arc::clone(&config));
widget.move_caret(widget.moved(10, 21, 34), Point::new(0, 0));
let result = widget.line_position();
let expected: usize = 34;
assert_eq!(result, expected);
}
}
#[cfg(test)]
mod test_render_box {
use crate::renderer::*;
use crate::tests::*;
use crate::ui::*;
use sdl2::pixels::*;
use sdl2::rect::*;
use std::sync::*;
#[test]
fn must_return_top_left_point() {
let config = support::build_config();
let widget = Caret::new(Arc::clone(&config));
let result = widget.render_start_point();
let expected = Point::new(0, 0);
assert_eq!(result, expected);
}
}
#[cfg(test)]
mod test_click_handler {
use crate::app::*;
use crate::renderer::*;
use crate::tests::*;
use crate::ui::*;
use sdl2::pixels::*;
use sdl2::rect::*;
use std::sync::*;
#[test]
fn refute_when_not_click_target() {
let config = support::build_config();
let widget = Caret::new(Arc::clone(&config));
let point = Point::new(9999, 9999);
let context = UpdateContext::Nothing;
let result = widget.is_left_click_target(&point, &context);
assert_eq!(result, false);
}
#[test]
fn assert_when_click_target() {
let config = support::build_config();
let widget = Caret::new(Arc::clone(&config));
let point = Point::new(0, 0);
let context = UpdateContext::Nothing;
let result = widget.is_left_click_target(&point, &context);
assert_eq!(result, true);
}
#[test]
fn refute_when_not_click_target_because_parent() {
let config = support::build_config();
let widget = Caret::new(Arc::clone(&config));
let point = Point::new(20, 30);
let context = UpdateContext::ParentPosition(Point::new(9999, 9999));
let result = widget.is_left_click_target(&point, &context);
assert_eq!(result, false);
}
#[test]
fn assert_when_click_target_because_parent() {
let config = support::build_config();
let widget = Caret::new(Arc::clone(&config));
let point = Point::new(10, 10);
let context = UpdateContext::ParentPosition(Point::new(10, 10));
let result = widget.is_left_click_target(&point, &context);
assert_eq!(result, true);
}
#[test]
fn assert_on_click_do_nothing() {
let config = support::build_config();
let mut widget = Caret::new(Arc::clone(&config));
let point = Point::new(12, 34);
let context = UpdateContext::ParentPosition(Point::new(678, 293));
let result = widget.on_left_click(&point, &context);
let expected = UpdateResult::NoOp;
assert_eq!(result, expected);
}
}

View File

@ -0,0 +1,43 @@
use sdl2::pixels::Color;
#[derive(Clone, Debug, PartialEq)]
pub struct CaretColor {
bright: Color,
blur: Color,
}
impl CaretColor {
pub fn new(bright: Color, blur: Color) -> Self {
Self { bright, blur }
}
pub fn bright(&self) -> &Color {
&self.bright
}
pub fn blur(&self) -> &Color {
&self.blur
}
}
#[cfg(test)]
mod test_getters {
use super::*;
use sdl2::pixels::*;
#[test]
fn assert_bright() {
let target = CaretColor::new(Color::RGBA(1, 2, 3, 4), Color::RGBA(5, 6, 7, 8));
let result = target.bright().clone();
let expected = Color::RGBA(1, 2, 3, 4);
assert_eq!(result, expected);
}
#[test]
fn assert_blur() {
let target = CaretColor::new(Color::RGBA(1, 2, 3, 4), Color::RGBA(5, 6, 7, 8));
let result = target.blur().clone();
let expected = Color::RGBA(5, 6, 7, 8);
assert_eq!(result, expected);
}
}

View File

@ -0,0 +1,123 @@
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CaretPosition {
text_position: usize,
line_number: usize,
line_position: usize,
}
impl CaretPosition {
pub fn new(text_position: usize, line_number: usize, line_position: usize) -> Self {
Self {
text_position,
line_number,
line_position,
}
}
pub fn text_position(&self) -> usize {
self.text_position
}
pub fn line_number(&self) -> usize {
self.line_number
}
pub fn line_position(&self) -> usize {
self.line_position
}
pub fn reset(&mut self) {
self.text_position = 0;
self.line_number = 0;
self.line_position = 0;
}
pub fn set_text_position(&mut self, n: usize) {
self.text_position = n;
}
pub fn set_line_number(&mut self, n: usize) {
self.line_number = n;
}
pub fn set_line_position(&mut self, n: usize) {
self.line_position = n;
}
pub fn moved(&self, text_position: i32, line_number: i32, line_position: i32) -> Self {
Self {
text_position: (self.text_position as i32 + text_position) as usize,
line_number: (self.line_number as i32 + line_number) as usize,
line_position: (self.line_position as i32 + line_position) as usize,
}
}
}
#[cfg(test)]
mod test_getters {
use super::*;
#[test]
fn assert_text_position() {
let target = CaretPosition::new(1, 2, 3);
let result = target.text_position();
let expected = 1;
assert_eq!(result, expected);
}
#[test]
fn assert_line_number() {
let target = CaretPosition::new(1, 2, 3);
let result = target.line_number();
let expected = 2;
assert_eq!(result, expected);
}
#[test]
fn assert_line_position() {
let target = CaretPosition::new(1, 2, 3);
let result = target.line_position();
let expected = 3;
assert_eq!(result, expected);
}
#[test]
fn assert_moved() {
let target = CaretPosition::new(1, 2, 3);
let result = target.moved(1, 2, 3);
let expected = CaretPosition::new(2, 4, 6);
assert_eq!(result, expected);
}
}
#[cfg(test)]
mod test_setters {
use super::*;
#[test]
fn assert_text_position() {
let mut target = CaretPosition::new(1, 2, 3);
target.set_text_position(100);
let result = target.text_position();
let expected = 100;
assert_eq!(result, expected);
}
#[test]
fn assert_line_number() {
let mut target = CaretPosition::new(1, 2, 3);
target.set_line_number(200);
let result = target.line_number();
let expected = 200;
assert_eq!(result, expected);
}
#[test]
fn assert_line_position() {
let mut target = CaretPosition::new(1, 2, 3);
target.set_line_position(300);
let result = target.line_position();
let expected = 300;
assert_eq!(result, expected);
}
}

21
src/ui/caret/mod.rs Normal file
View File

@ -0,0 +1,21 @@
#[derive(Clone, Debug, PartialEq)]
pub enum CaretState {
Bright,
Blur,
}
#[derive(Clone, Debug, PartialEq)]
pub enum MoveDirection {
Left,
Right,
Up,
Down,
}
pub mod caret;
pub mod caret_color;
pub mod caret_position;
pub use crate::ui::caret::caret::*;
pub use crate::ui::caret::caret_color::*;
pub use crate::ui::caret::caret_position::*;

View File

@ -13,7 +13,7 @@ use crate::ui::*;
pub struct EditorFile {
path: String,
sections: Vec<EditorFileSection>,
render_position: Rect,
dest: Rect,
buffer: String,
config: Arc<RwLock<Config>>,
line_height: u32,
@ -42,7 +42,7 @@ impl EditorFile {
Self {
path,
sections,
render_position,
dest: render_position,
buffer,
config,
line_height: 0,
@ -66,7 +66,7 @@ impl EditorFile {
}
pub fn render_position(&self) -> &Rect {
&self.render_position
&self.dest
}
pub fn get_character_at(&self, index: usize) -> Option<TextCharacter> {
@ -119,11 +119,10 @@ impl EditorFile {
}
impl Render for EditorFile {
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) -> UR {
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) {
for section in self.sections.iter() {
section.render(canvas, renderer, parent);
}
UR::NoOp
}
fn prepare_ui(&mut self, renderer: &mut Renderer) {
@ -179,6 +178,10 @@ impl ClickHandler for EditorFile {
impl RenderBox for EditorFile {
fn render_start_point(&self) -> Point {
self.render_position.top_left()
self.dest.top_left()
}
fn dest(&self) -> &Rect {
&self.dest
}
}

View File

@ -101,11 +101,10 @@ impl EditorFileSection {
}
impl Render for EditorFileSection {
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) -> UR {
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) {
for token in self.tokens.iter() {
token.render(canvas, renderer, parent);
}
UR::NoOp
}
fn prepare_ui(&mut self, renderer: &mut Renderer) {

View File

@ -1,8 +1,8 @@
use crate::app::{UpdateResult as UR, WindowCanvas as WC};
use crate::config::Config;
use crate::config::*;
use crate::lexer::TokenType;
use crate::renderer::managers::{FontDetails, TextDetails};
use crate::renderer::Renderer;
use crate::renderer::*;
use crate::ui::*;
use sdl2::pixels::Color;
use sdl2::rect::{Point, Rect};
@ -19,7 +19,6 @@ impl TokenType {
&TokenType::Whitespace { .. } => ch.whitespace().color().into(),
&TokenType::Keyword { .. } => ch.keyword().color().into(),
&TokenType::String { .. } => ch.string().color().into(),
&TokenType::Number { .. } => ch.number().color().into(),
&TokenType::Identifier { .. } => ch.identifier().color().into(),
&TokenType::Literal { .. } => ch.literal().color().into(),
&TokenType::Comment { .. } => ch.comment().color().into(),
@ -113,14 +112,13 @@ impl Render for EditorFileToken {
* Must first create targets so even if new line appear renderer will know
* where move render starting point
*/
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) -> UR {
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) {
if self.token_type.is_new_line() {
return UR::NoOp;
return;
}
for text_character in self.characters.iter() {
text_character.render(canvas, renderer, parent);
}
UR::NoOp
}
fn prepare_ui(&mut self, renderer: &mut Renderer) {

View File

@ -6,6 +6,7 @@ use std::sync::*;
use crate::app::*;
use crate::app::{UpdateResult as UR, WindowCanvas as WS};
use crate::config::*;
use crate::ui::*;
pub struct FileEditor {
@ -13,11 +14,11 @@ pub struct FileEditor {
scroll: Point,
caret: Caret,
file: Option<EditorFile>,
config: Arc<RwLock<Config>>,
config: ConfigAccess,
}
impl FileEditor {
pub fn new(config: Arc<RwLock<Config>>) -> Self {
pub fn new(config: ConfigAccess) -> Self {
let dest = {
let c = config.read().unwrap();
Rect::new(
@ -36,10 +37,6 @@ impl FileEditor {
}
}
pub fn config(&self) -> &Arc<RwLock<Config>> {
&self.config
}
pub fn caret(&self) -> &Caret {
&self.caret
}
@ -166,13 +163,13 @@ impl FileEditor {
}
impl Render for FileEditor {
fn render(&self, canvas: &mut WS, renderer: &mut Renderer, _parent: Parent) -> UR {
fn render(&self, canvas: &mut WS, renderer: &mut Renderer, _parent: Parent) {
canvas.set_clip_rect(self.dest.clone());
match self.file() {
Some(file) => file.render(canvas, renderer, Some(self)),
_ => UR::NoOp,
_ => (),
};
self.caret.render(canvas, renderer, Some(self))
self.caret.render(canvas, renderer, Some(self));
}
fn prepare_ui(&mut self, renderer: &mut Renderer) {
@ -229,6 +226,16 @@ impl RenderBox for FileEditor {
fn render_start_point(&self) -> Point {
self.dest.top_left() + self.scroll
}
fn dest(&self) -> &Rect {
&self.dest
}
}
impl ConfigHolder for FileEditor {
fn config(&self) -> &ConfigAccess {
&self.config
}
}
#[cfg(test)]
@ -256,35 +263,4 @@ mod tests {
assert_eq!(file.path(), first_file.path());
assert_eq!(file.buffer(), first_file.buffer());
}
#[test]
fn add_text() {
let config = Arc::new(RwLock::new(Config::new()));
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
let window = video_subsystem
.window("Test", 1, 1)
.borderless()
.opengl()
.build()
.unwrap();
let canvas = window.into_canvas().accelerated().build().unwrap();
let font_context = sdl2::ttf::init().unwrap();
let texture_creator = canvas.texture_creator();
let mut renderer = Renderer::new(config.clone(), &font_context, &texture_creator);
let mut editor = FileEditor::new(Arc::clone(&config));
let mut file = EditorFile::new("./foo.txt".to_string(), "foo".to_string(), config.clone());
file.prepare_ui(&mut renderer);
assert_eq!(editor.open_file(file).is_none(), true);
assert_eq!(editor.caret().position().text_position(), 0);
assert_eq!(editor.file().is_some(), true);
assert_eq!(editor.file().unwrap().sections().len(), 1);
assert_eq!(editor.file().unwrap().get_character_at(0).is_some(), true);
editor.insert_text("z".to_string(), &mut renderer);
assert_eq!(editor.caret().position().text_position(), 1);
assert_eq!(editor.file().is_some(), true);
assert_eq!(editor.file().unwrap().buffer(), "zfoo".to_string());
}
}

View File

@ -1,6 +1,6 @@
use crate::app::{UpdateResult as UR, WindowCanvas as WC};
use crate::config::Config;
use crate::renderer::Renderer;
use crate::config::*;
use crate::renderer::*;
use crate::ui::*;
use sdl2::pixels::Color;
use sdl2::rect::{Point, Rect};
@ -11,33 +11,30 @@ pub struct MenuBar {
border_color: Color,
background_color: Color,
dest: Rect,
config: Arc<RwLock<Config>>,
pending: bool,
config: ConfigAccess,
}
impl MenuBar {
pub fn new(config: Arc<RwLock<Config>>) -> Self {
let background_color = { config.read().unwrap().theme().background().into() };
pub fn new(config: ConfigAccess) -> Self {
let (background_color, w, h): (Color, u32, u16) = {
let c = config.read().unwrap();
(c.theme().background().into(), c.width(), c.menu_height())
};
Self {
border_color: Color::RGB(10, 10, 10),
background_color,
dest: Rect::new(0, 0, 0, 0),
dest: Rect::new(0, 0, w as u32, h as u32),
config,
pending: true,
}
}
pub fn background_color(&self) -> &Color {
&self.background_color
}
pub fn dest(&self) -> &Rect {
&self.dest
}
}
impl Render for MenuBar {
fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, parent: Parent) -> UR {
fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, parent: Parent) {
canvas.set_clip_rect(self.dest.clone());
canvas.set_draw_color(self.background_color.clone());
canvas
@ -54,18 +51,12 @@ impl Render for MenuBar {
Some(parent) => move_render_point(parent.render_start_point(), self.dest()),
})
.unwrap_or_else(|_| panic!("Failed to draw main menu background"));
UR::NoOp
}
fn prepare_ui(&mut self, _renderer: &mut Renderer) {
if !self.pending {
return;
}
let width = self.config.read().unwrap().width();
let height = self.config.read().unwrap().menu_height() as u32;
self.dest = Rect::new(0, 0, width, height);
self.pending = false;
}
}
@ -79,17 +70,15 @@ impl Update for MenuBar {
impl ClickHandler for MenuBar {
fn on_left_click(&mut self, _point: &Point, _context: &UpdateContext) -> UR {
unimplemented!()
UR::NoOp
}
fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool {
is_in_rect(
point,
&match context {
&UpdateContext::ParentPosition(p) => move_render_point(p, self.dest()),
let rect = match context.clone() {
UpdateContext::ParentPosition(p) => move_render_point(p.clone(), self.dest()),
_ => self.dest().clone(),
},
)
};
is_in_rect(point, &rect)
}
}
@ -97,4 +86,128 @@ impl RenderBox for MenuBar {
fn render_start_point(&self) -> Point {
self.dest.top_left()
}
fn dest(&self) -> &Rect {
&self.dest
}
}
#[cfg(test)]
mod test_getters {
use crate::app::*;
use crate::renderer::*;
use crate::tests::*;
use crate::ui::*;
use sdl2::pixels::*;
use sdl2::rect::*;
use std::sync::*;
#[test]
fn assert_background_color() {
let config = support::build_config();
let widget = MenuBar::new(Arc::clone(&config));
let result = widget.background_color().clone();
let expected = Color::RGBA(18, 18, 18, 0);
assert_eq!(result, expected);
}
#[test]
fn assert_dest() {
let config = support::build_config();
let (w, h) = {
let c = config.read().unwrap();
(c.width() as u32, c.menu_height() as u32)
};
let widget = MenuBar::new(Arc::clone(&config));
let result = widget.dest().clone();
let expected = Rect::new(0, 0, w, h);
assert_eq!(result, expected);
}
}
#[cfg(test)]
mod test_render_box {
use crate::renderer::*;
use crate::tests::*;
use crate::ui::*;
use sdl2::pixels::*;
use sdl2::rect::*;
use std::sync::*;
#[test]
fn must_return_top_left_point() {
let config = support::build_config();
let widget = MenuBar::new(Arc::clone(&config));
let result = widget.render_start_point();
let expected = Point::new(0, 0);
assert_eq!(result, expected);
}
}
#[cfg(test)]
mod test_click_handler {
use crate::app::*;
use crate::renderer::*;
use crate::tests::*;
use crate::ui::*;
use sdl2::pixels::*;
use sdl2::rect::*;
use std::sync::*;
#[test]
fn refute_when_not_click_target() {
let config = support::build_config();
let widget = MenuBar::new(Arc::clone(&config));
let point = Point::new(9999, 9999);
let context = UpdateContext::Nothing;
let result = widget.is_left_click_target(&point, &context);
assert_eq!(result, false);
}
#[test]
fn assert_when_click_target() {
let config = support::build_config();
let widget = MenuBar::new(Arc::clone(&config));
let point = Point::new(20, 30);
let context = UpdateContext::Nothing;
let result = widget.is_left_click_target(&point, &context);
assert_eq!(result, true);
}
#[test]
fn refute_when_not_click_target_because_parent() {
let config = support::build_config();
let widget = MenuBar::new(Arc::clone(&config));
let point = Point::new(20, 30);
let context = UpdateContext::ParentPosition(Point::new(9999, 9999));
let result = widget.is_left_click_target(&point, &context);
assert_eq!(result, false);
}
#[test]
fn assert_when_click_target_because_parent() {
let config = support::build_config();
let (w, h) = {
(
config.read().unwrap().width(),
config.read().unwrap().menu_height(),
)
};
let widget = MenuBar::new(Arc::clone(&config));
let point = Point::new(w as i32 + 120, h as i32 + 130);
let context = UpdateContext::ParentPosition(Point::new(130, 140));
let result = widget.is_left_click_target(&point, &context);
assert_eq!(result, true);
}
#[test]
fn assert_on_click_do_nothing() {
let config = support::build_config();
let mut widget = MenuBar::new(Arc::clone(&config));
let point = Point::new(12, 34);
let context = UpdateContext::ParentPosition(Point::new(678, 293));
let result = widget.on_left_click(&point, &context);
let expected = UpdateResult::NoOp;
assert_eq!(result, expected);
}
}

View File

@ -1,8 +1,8 @@
use sdl2::rect::{Point, Rect};
use crate::app::{UpdateResult, WindowCanvas};
use crate::config::Config;
use crate::renderer::managers::FontDetails;
use crate::app::{UpdateResult as UR, WindowCanvas as WC};
use crate::config::*;
use crate::renderer::managers::*;
use crate::renderer::Renderer;
pub mod caret;
@ -30,41 +30,32 @@ pub enum UpdateContext<'l> {
#[inline]
pub fn is_in_rect(point: &Point, rect: &Rect) -> bool {
let start = rect.top_left();
let end = Point::new(
rect.x() + (rect.width() as i32),
rect.y() + (rect.height() as i32),
);
start.x() <= point.x() && start.y() <= point.y() && end.x() >= point.x() && end.y() >= point.y()
rect.contains_point(point.clone())
}
pub fn get_text_character_rect(c: char, renderer: &mut Renderer) -> Option<Rect> {
let font_details = FontDetails::new(
#[inline]
pub fn build_font_details<T>(config_holder: &T) -> FontDetails
where
T: ConfigHolder,
{
let c = config_holder.config().read().unwrap();
FontDetails::new(
c.editor_config().font_path().as_str(),
c.editor_config().character_size().clone(),
)
}
pub fn get_text_character_rect<'l, T>(c: char, renderer: &mut T) -> Option<Rect>
where
T: ManagersHolder<'l> + ConfigHolder,
{
let font_details = build_font_details(renderer);
renderer
.config()
.read()
.unwrap()
.editor_config()
.font_path()
.as_str(),
renderer
.config()
.read()
.unwrap()
.editor_config()
.character_size()
.clone(),
);
let font = renderer
.font_manager()
.load(&font_details)
.unwrap_or_else(|_| panic!("Font not found {:?}", font_details));
if let Ok((width, height)) = font.size_of_char(c) {
Some(Rect::new(0, 0, width, height))
} else {
None
}
.ok()
.and_then(|font| font.size_of_char(c).ok())
.and_then(|(width, height)| Some(Rect::new(0, 0, width, height)))
}
#[inline]
@ -73,26 +64,73 @@ pub fn move_render_point(p: Point, d: &Rect) -> Rect {
}
pub trait Render {
fn render(
&self,
canvas: &mut WindowCanvas,
renderer: &mut Renderer,
parent: Parent,
) -> UpdateResult;
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent);
fn prepare_ui(&mut self, renderer: &mut Renderer);
}
pub trait Update {
fn update(&mut self, ticks: i32, context: &UpdateContext) -> UpdateResult;
fn update(&mut self, ticks: i32, context: &UpdateContext) -> UR;
}
pub trait ClickHandler {
fn on_left_click(&mut self, point: &Point, context: &UpdateContext) -> UpdateResult;
fn on_left_click(&mut self, point: &Point, context: &UpdateContext) -> UR;
fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool;
}
pub trait RenderBox {
fn render_start_point(&self) -> Point;
fn dest(&self) -> &Rect;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::support;
use sdl2::rect::*;
struct ConfigWrapper {
pub inner: ConfigAccess,
}
impl ConfigHolder for ConfigWrapper {
fn config(&self) -> &ConfigAccess {
&self.inner
}
}
#[test]
fn must_return_true_if_inside_rect() {
let rect = Rect::new(10, 10, 30, 30);
let point = Point::new(20, 20);
assert_eq!(is_in_rect(&point, &rect), true);
}
#[test]
fn must_return_not_if_not_inside_rect() {
let rect = Rect::new(10, 10, 30, 30);
let point = Point::new(41, 41);
assert_eq!(is_in_rect(&point, &rect), false);
}
#[test]
fn must_return_moved_rect() {
let rect = Rect::new(10, 20, 30, 40);
let point = Point::new(11, 11);
assert_eq!(move_render_point(point, &rect), Rect::new(21, 31, 30, 40));
}
#[test]
fn must_build_font_details() {
let config = support::build_config();
let wrapper = ConfigWrapper {
inner: config.clone(),
};
let details = build_font_details(&wrapper);
let c = config.read().unwrap();
assert_eq!(details.path, c.editor_config().font_path().to_string());
assert_eq!(details.size, c.editor_config().character_size());
}
}

View File

@ -1,8 +1,8 @@
use crate::app::{UpdateResult as UR, WindowCanvas as WC};
use crate::config::Config;
use crate::config::*;
use crate::lexer::TokenType;
use crate::renderer::managers::{FontDetails, TextDetails};
use crate::renderer::Renderer;
use crate::renderer::managers::*;
use crate::renderer::*;
use crate::ui::caret::CaretPosition;
use crate::ui::*;
@ -22,7 +22,7 @@ pub struct TextCharacter {
source: Rect,
dest: Rect,
color: Color,
config: Arc<RwLock<Config>>,
config: ConfigAccess,
}
impl TextCharacter {
@ -32,7 +32,7 @@ impl TextCharacter {
line: usize,
last_in_line: bool,
color: Color,
config: Arc<RwLock<Config>>,
config: ConfigAccess,
) -> Self {
Self {
text_character,
@ -50,14 +50,18 @@ impl TextCharacter {
self.last_in_line
}
pub fn dest(&self) -> &Rect {
&self.dest
}
pub fn source(&self) -> &Rect {
&self.source
}
pub fn set_dest(&mut self, rect: &Rect) {
self.dest = rect.clone();
}
pub fn set_source(&mut self, rect: &Rect) {
self.source = rect.clone();
}
pub fn color(&self) -> &Color {
&self.color
}
@ -101,9 +105,9 @@ impl Render for TextCharacter {
* Must first create targets so even if new line appear renderer will know
* where move render starting point
*/
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) -> UR {
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) {
if self.is_new_line() {
return UR::NoOp;
return;
}
let font_details = {
@ -127,20 +131,26 @@ impl Render for TextCharacter {
Some(parent) => move_render_point(parent.render_start_point(), self.dest()),
};
if let Ok(texture) = renderer.texture_manager().load_text(&mut details, &font) {
renderer.render_texture(canvas, &texture, &self.source, &dest);
canvas
.copy_ex(
&texture,
Some(self.source.clone()),
Some(dest.clone()),
0.0,
None,
false,
false,
)
.unwrap();
}
// let c = Color::RGB(255, 0, 0);
// canvas.set_draw_color(c);
// canvas.draw_rect(dest.clone()).unwrap();
UR::NoOp
}
fn prepare_ui(&mut self, renderer: &mut Renderer) {
let font_details = {
let config = renderer.config().read().unwrap();
let ec = config.editor_config();
FontDetails::new(ec.font_path().as_str(), ec.character_size().clone())
};
let font_details = build_font_details(renderer);
let font = renderer
.font_manager()
.load(&font_details)
@ -151,8 +161,8 @@ impl Render for TextCharacter {
c => c,
};
if let Some(rect) = get_text_character_rect(c, renderer) {
self.source = rect.clone();
self.dest = rect.clone();
self.set_source(&rect);
self.set_dest(&rect);
}
let mut details = TextDetails {
text: self.text_character.to_string(),
@ -195,4 +205,311 @@ impl RenderBox for TextCharacter {
fn render_start_point(&self) -> Point {
self.dest.top_left()
}
fn dest(&self) -> &Rect {
&self.dest
}
}
#[cfg(test)]
mod test_getters {
use crate::renderer::*;
use crate::tests::*;
use crate::ui::*;
use sdl2::pixels::*;
use sdl2::rect::*;
use std::sync::*;
#[test]
fn must_return_valid_is_last_in_line() {
let config = support::build_config();
let widget = TextCharacter::new(
'\n',
0,
0,
true,
Color::RGB(1, 12, 123),
Arc::clone(&config),
);
assert_eq!(widget.is_last_in_line(), true);
}
#[test]
fn must_return_true_for_is_new_line_if_new_line() {
let config = support::build_config();
let widget = TextCharacter::new(
'\n',
0,
0,
true,
Color::RGB(1, 12, 123),
Arc::clone(&config),
);
assert_eq!(widget.is_new_line(), true);
}
#[test]
fn must_return_false_for_is_new_line_if_new_line() {
let config = support::build_config();
let widget =
TextCharacter::new('W', 0, 0, true, Color::RGB(1, 12, 123), Arc::clone(&config));
assert_eq!(widget.is_new_line(), false);
}
#[test]
fn must_return_valid_position() {
let config = support::build_config();
let widget = TextCharacter::new(
'\n',
1,
123,
true,
Color::RGB(1, 12, 123),
Arc::clone(&config),
);
assert_eq!(widget.position(), 1);
}
#[test]
fn must_return_valid_line() {
let config = support::build_config();
let widget = TextCharacter::new(
'\n',
1,
123,
true,
Color::RGB(1, 12, 123),
Arc::clone(&config),
);
assert_eq!(widget.line(), 123);
}
#[test]
fn must_return_valid_text_character() {
let config = support::build_config();
let widget = TextCharacter::new(
'\n',
87,
123,
true,
Color::RGB(1, 12, 123),
Arc::clone(&config),
);
assert_eq!(widget.text_character(), '\n');
}
#[test]
fn must_return_valid_source() {
let config = support::build_config();
let widget = TextCharacter::new(
'\n',
0,
0,
true,
Color::RGB(1, 12, 123),
Arc::clone(&config),
);
assert_eq!(widget.source(), &Rect::new(0, 0, 0, 0));
}
#[test]
fn must_return_valid_dest() {
let config = support::build_config();
let widget = TextCharacter::new(
'\n',
0,
0,
true,
Color::RGB(1, 12, 123),
Arc::clone(&config),
);
assert_eq!(widget.dest(), &Rect::new(0, 0, 0, 0));
}
#[test]
fn must_return_valid_color() {
let config = support::build_config();
let widget = TextCharacter::new(
'\n',
0,
0,
true,
Color::RGB(1, 12, 123),
Arc::clone(&config),
);
assert_eq!(widget.color(), &Color::RGB(1, 12, 123));
}
}
#[cfg(test)]
mod test_own_methods {
use crate::renderer::*;
use crate::tests::*;
use crate::ui::*;
use sdl2::pixels::*;
use sdl2::rect::*;
use std::sync::*;
#[test]
fn must_update_position_of_new_line() {
let config = support::build_config();
let mut widget =
TextCharacter::new('\n', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config));
widget.set_dest(&Rect::new(10, 20, 30, 40));
widget.set_source(&Rect::new(50, 60, 70, 80));
let mut current = Rect::new(10, 23, 0, 0);
widget.update_position(&mut current);
assert_eq!(current, Rect::new(0, 103, 1, 1));
assert_eq!(widget.dest(), &Rect::new(0, 103, 30, 40));
assert_eq!(widget.source(), &Rect::new(50, 60, 70, 80));
}
#[test]
fn must_update_position_of_non_new_line() {
let config = support::build_config();
let mut widget =
TextCharacter::new('W', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config));
widget.set_dest(&Rect::new(10, 20, 30, 40));
widget.set_source(&Rect::new(50, 60, 70, 80));
let mut current = Rect::new(10, 23, 0, 0);
widget.update_position(&mut current);
assert_eq!(current, Rect::new(80, 23, 1, 1));
assert_eq!(widget.dest(), &Rect::new(10, 23, 70, 80));
assert_eq!(widget.source(), &Rect::new(50, 60, 70, 80));
}
}
#[cfg(test)]
mod test_click_handler {
use crate::app::*;
use crate::renderer::*;
use crate::tests::*;
use crate::ui::*;
use sdl2::pixels::*;
use sdl2::rect::*;
use std::sync::*;
#[test]
fn refute_when_not_click_target() {
let config = support::build_config();
let mut widget =
TextCharacter::new('\n', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config));
widget.set_dest(&Rect::new(10, 20, 30, 40));
widget.set_source(&Rect::new(50, 60, 70, 80));
let point = Point::new(0, 0);
let context = UpdateContext::Nothing;
let result = widget.is_left_click_target(&point, &context);
assert_eq!(result, false);
}
#[test]
fn assert_when_click_target() {
let config = support::build_config();
let mut widget =
TextCharacter::new('\n', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config));
widget.set_dest(&Rect::new(10, 20, 30, 40));
widget.set_source(&Rect::new(50, 60, 70, 80));
let point = Point::new(20, 30);
let context = UpdateContext::Nothing;
let result = widget.is_left_click_target(&point, &context);
assert_eq!(result, true);
}
#[test]
fn refute_when_not_click_target_because_parent() {
let config = support::build_config();
let mut widget =
TextCharacter::new('\n', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config));
widget.set_dest(&Rect::new(10, 20, 30, 40));
widget.set_source(&Rect::new(50, 60, 70, 80));
let point = Point::new(20, 30);
let context = UpdateContext::ParentPosition(Point::new(100, 100));
let result = widget.is_left_click_target(&point, &context);
assert_eq!(result, false);
}
#[test]
fn assert_when_click_target_because_parent() {
let config = support::build_config();
let mut widget =
TextCharacter::new('\n', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config));
widget.set_dest(&Rect::new(10, 20, 30, 40));
widget.set_source(&Rect::new(50, 60, 70, 80));
let point = Point::new(120, 130);
let context = UpdateContext::ParentPosition(Point::new(100, 100));
let result = widget.is_left_click_target(&point, &context);
assert_eq!(result, true);
}
#[test]
fn assert_on_click_return_move_caret() {
let config = support::build_config();
let position = 1233;
let line = 2893;
let mut widget = TextCharacter::new(
'\n',
position.clone(),
line.clone(),
true,
Color::RGB(0, 0, 0),
Arc::clone(&config),
);
let dest = Rect::new(10, 20, 30, 40);
widget.set_dest(&dest);
widget.set_source(&Rect::new(50, 60, 70, 80));
let point = Point::new(12, 34);
let context = UpdateContext::ParentPosition(Point::new(678, 293));
let result = widget.on_left_click(&point, &context);
let expected = UpdateResult::MoveCaret(dest, CaretPosition::new(position, line, 0));
assert_eq!(result, expected);
}
}
#[cfg(test)]
mod test_render_box {
use crate::renderer::*;
use crate::tests::*;
use crate::ui::*;
use sdl2::pixels::*;
use sdl2::rect::*;
use std::sync::*;
#[test]
fn must_return_top_left_point() {
let config = support::build_config();
let mut widget =
TextCharacter::new('\n', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config));
widget.set_dest(&Rect::new(10, 20, 30, 40));
widget.set_source(&Rect::new(50, 60, 70, 80));
let result = widget.render_start_point();
let expected = Point::new(10, 20);
assert_eq!(result, expected);
}
}
#[cfg(test)]
mod test_update {
use crate::app::*;
use crate::renderer::*;
use crate::tests::*;
use crate::ui::*;
use sdl2::pixels::*;
use sdl2::rect::*;
use std::sync::*;
#[test]
fn assert_do_nothing() {
let config = support::build_config();
let mut widget =
TextCharacter::new('\n', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config));
widget.set_dest(&Rect::new(10, 20, 30, 40));
widget.set_source(&Rect::new(50, 60, 70, 80));
let result = widget.update(
3234,
&UpdateContext::ParentPosition(Point::new(234, 234234)),
);
let expected = UpdateResult::NoOp;
assert_eq!(result, expected);
}
}