diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..39d48c1 --- /dev/null +++ b/.circleci/config.yml @@ -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" diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..d4cbf78 --- /dev/null +++ b/.codecov.yml @@ -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 diff --git a/.gitignore b/.gitignore index a626103..893adbc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ .idea log .rider +.codecov +cobertura.xml diff --git a/Cargo.lock b/Cargo.lock index b9a5477..1f75337 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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)", diff --git a/Cargo.toml b/Cargo.toml index c1aa22b..6fd482d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ serde_derive = "*" log = "*" env_logger = "*" simplelog = "*" +lazy_static = "*" [dependencies.sdl2] version = "0.31.0" diff --git a/README.md b/README.md index 17d1185..25538f6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/guitest/main.rs b/guitest/main.rs new file mode 100644 index 0000000..0939fe1 --- /dev/null +++ b/guitest/main.rs @@ -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(); + +} diff --git a/src/app/app_state.rs b/src/app/app_state.rs index b529e92..0bdf3be 100644 --- a/src/app/app_state.rs +++ b/src/app/app_state.rs @@ -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> { - &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 + } +} diff --git a/src/app/application.rs b/src/app/application.rs index 69cc43d..1ecdeb1 100644 --- a/src/app/application.rs +++ b/src/app/application.rs @@ -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> { +impl ConfigHolder for Application { + fn config(&self) -> &ConfigAccess { &self.config } } diff --git a/src/config/config.rs b/src/config/config.rs new file mode 100644 index 0000000..bb2a3f4 --- /dev/null +++ b/src/config/config.rs @@ -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; + +#[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 = mapping.keys().map(|s| s.to_string()).collect(); + let mut expected: Vec = + vec![".".to_string(), "txt".to_string(), "rs".to_string()]; + keys.sort(); + expected.sort(); + assert_eq!(keys, expected); + } + { + let mut keys: Vec = mapping.values().map(|s| s.clone()).collect(); + let mut expected: Vec = + vec![Language::PlainText, Language::PlainText, Language::Rust]; + keys.sort(); + expected.sort(); + assert_eq!(keys, expected); + } + } +} diff --git a/src/config/creator.rs b/src/config/creator.rs index ba038af..8a3db7b 100644 --- a/src/config/creator.rs +++ b/src/config/creator.rs @@ -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!")); } } diff --git a/src/config/editor_config.rs b/src/config/editor_config.rs new file mode 100644 index 0000000..cb6e6b9 --- /dev/null +++ b/src/config/editor_config.rs @@ -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 + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index cef019c..7b7514b 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -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; +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, -} - -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 type ConfigAccess = Arc>; + +pub trait ConfigHolder { + fn config(&self) -> &ConfigAccess; } diff --git a/src/lexer/mod.rs b/src/lexer/mod.rs index b3a5377..f954299 100644 --- a/src/lexer/mod.rs +++ b/src/lexer/mod.rs @@ -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 { .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); + } +} diff --git a/src/lexer/plain.rs b/src/lexer/plain.rs index 1283614..4244719 100644 --- a/src/lexer/plain.rs +++ b/src/lexer/plain.rs @@ -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 = lexer.map(|pair| pair.0).collect(); + let expected: Vec = 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 = lexer.map(|pair| pair.0).collect(); + let expected: Vec = 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 = lexer.map(|pair| pair.0).collect(); + let expected: Vec = 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 = lexer.map(|pair| pair.0).collect(); + let expected: Vec = 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 = lexer.map(|pair| pair.0).collect(); + let expected: Vec = 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 = lexer.map(|pair| pair.0).collect(); + let expected: Vec = 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); + } +} diff --git a/src/lexer/rust_lang.rs b/src/lexer/rust_lang.rs index 4d1e52b..c0551fb 100644 --- a/src/lexer/rust_lang.rs +++ b/src/lexer/rust_lang.rs @@ -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 = lexer.map(|pair| pair.0).collect(); + let expected: Vec = 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 = lexer.map(|pair| pair.0).collect(); + let expected: Vec = 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 = lexer.map(|pair| pair.0).collect(); + let expected: Vec = 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 = lexer.map(|pair| pair.0).collect(); + let expected: Vec = 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 = lexer.map(|pair| pair.0).collect(); + let expected: Vec = 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 = lexer.map(|pair| pair.0).collect(); + let expected: Vec = 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 = lexer.map(|pair| pair.0).collect(); + let expected: Vec = 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 = lexer.map(|pair| pair.0).collect(); + let expected: Vec = 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 = lexer.map(|pair| pair.0).collect(); + let expected: Vec = 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 = lexer.map(|pair| pair.0).collect(); + let expected: Vec = 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 = lexer::Lexer::new(buffer).map(|p| p.0).collect(); + let expected: Vec = 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); + } +} diff --git a/src/main.rs b/src/main.rs index d1abbf4..ca5ddb7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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( - LevelFilter::Info, - Config::default(), - File::create(log_file_path).unwrap(), - ), - ]) - .unwrap(); + let mut outputs: Vec> = vec![WriteLogger::new( + LevelFilter::Info, + Config::default(), + File::create(log_file_path).unwrap(), + )]; + if let Some(term) = TermLogger::new(LevelFilter::Warn, Config::default()) { + outputs.push(term); + } + + CombinedLogger::init(outputs).unwrap(); } fn main() { diff --git a/src/renderer/managers.rs b/src/renderer/managers.rs index 1708400..673643a 100644 --- a/src/renderer/managers.rs +++ b/src/renderer/managers.rs @@ -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>; +pub type RcFont<'l> = Rc>; + 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>; 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, + ) -> Result>, String>; +} + +impl<'l, T> TextTextureManager<'l> for TextureManager<'l, T> { + //noinspection RsWrongLifetimeParametersNumber + fn load_text( &mut self, details: &mut TextDetails, font: &Rc, diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index d4e9daa..a1e5ced 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -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>, - font_manager: FontManager<'a>, - texture_manager: TextureManager<'a, WindowContext>, - scroll: Point, -} - -impl<'a> Renderer<'a> { - pub fn new( - config: Arc>, - font_context: &'a Sdl2TtfContext, - texture_creator: &'a TextureCreator, - ) -> Self { - Self { - config, - font_manager: FontManager::new(&font_context), - texture_manager: TextureManager::new(&texture_creator), - scroll: (0, 0).into(), - } - } - - pub fn config(&self) -> &Arc> { - &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, - 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> { - 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::*; diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs new file mode 100644 index 0000000..a872539 --- /dev/null +++ b/src/renderer/renderer.rs @@ -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, + ) -> 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 + } +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..04199f4 --- /dev/null +++ b/src/tests.rs @@ -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> { + 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() + } +} diff --git a/src/ui/caret.rs b/src/ui/caret.rs deleted file mode 100644 index aa73a15..0000000 --- a/src/ui/caret.rs +++ /dev/null @@ -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>) -> 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) -> &::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() - } -} diff --git a/src/ui/caret/caret.rs b/src/ui/caret/caret.rs new file mode 100644 index 0000000..919fa52 --- /dev/null +++ b/src/ui/caret/caret.rs @@ -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) -> &::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); + } +} diff --git a/src/ui/caret/caret_color.rs b/src/ui/caret/caret_color.rs new file mode 100644 index 0000000..98ec5d8 --- /dev/null +++ b/src/ui/caret/caret_color.rs @@ -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); + } +} diff --git a/src/ui/caret/caret_position.rs b/src/ui/caret/caret_position.rs new file mode 100644 index 0000000..d82a025 --- /dev/null +++ b/src/ui/caret/caret_position.rs @@ -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); + } +} diff --git a/src/ui/caret/mod.rs b/src/ui/caret/mod.rs new file mode 100644 index 0000000..928d15a --- /dev/null +++ b/src/ui/caret/mod.rs @@ -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::*; diff --git a/src/ui/file/editor_file.rs b/src/ui/file/editor_file.rs index d012fb4..e6c9a96 100644 --- a/src/ui/file/editor_file.rs +++ b/src/ui/file/editor_file.rs @@ -13,7 +13,7 @@ use crate::ui::*; pub struct EditorFile { path: String, sections: Vec, - render_position: Rect, + dest: Rect, buffer: String, config: Arc>, 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 { @@ -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 } } diff --git a/src/ui/file/editor_file_section.rs b/src/ui/file/editor_file_section.rs index ed616d9..dd1c225 100644 --- a/src/ui/file/editor_file_section.rs +++ b/src/ui/file/editor_file_section.rs @@ -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) { diff --git a/src/ui/file/editor_file_token.rs b/src/ui/file/editor_file_token.rs index 01ec080..4d244ee 100644 --- a/src/ui/file/editor_file_token.rs +++ b/src/ui/file/editor_file_token.rs @@ -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) { diff --git a/src/ui/file_editor.rs b/src/ui/file_editor.rs index 8c9fe98..8e5322e 100644 --- a/src/ui/file_editor.rs +++ b/src/ui/file_editor.rs @@ -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, - config: Arc>, + config: ConfigAccess, } impl FileEditor { - pub fn new(config: Arc>) -> 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> { - &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()); - } } diff --git a/src/ui/menu_bar.rs b/src/ui/menu_bar.rs index fbe120a..0529807 100644 --- a/src/ui/menu_bar.rs +++ b/src/ui/menu_bar.rs @@ -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>, - pending: bool, + config: ConfigAccess, } impl MenuBar { - pub fn new(config: Arc>) -> 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()), - _ => self.dest().clone(), - }, - ) + 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); + } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index aae6f32..ffeaa9b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -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 { - let font_details = FontDetails::new( - renderer - .config() - .read() - .unwrap() - .editor_config() - .font_path() - .as_str(), - renderer - .config() - .read() - .unwrap() - .editor_config() - .character_size() - .clone(), - ); - let font = renderer +#[inline] +pub fn build_font_details(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 +where + T: ManagersHolder<'l> + ConfigHolder, +{ + let font_details = build_font_details(renderer); + 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()); + } } diff --git a/src/ui/text_character.rs b/src/ui/text_character.rs index 02290c2..e626cc0 100644 --- a/src/ui/text_character.rs +++ b/src/ui/text_character.rs @@ -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>, + config: ConfigAccess, } impl TextCharacter { @@ -32,7 +32,7 @@ impl TextCharacter { line: usize, last_in_line: bool, color: Color, - config: Arc>, + 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); + } }