diff --git a/README.md b/README.md index 25538f6..899b209 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,17 @@ +[![codecov](https://codecov.io/gh/Eraden/rider/branch/master/graph/badge.svg)](https://codecov.io/gh/Eraden/rider) +[![CircleCI](https://circleci.com/gh/Eraden/rider.svg?style=svg&circle-token=546aae50b559665bd1f77a6452eff25e26a9d966)](https://circleci.com/gh/Eraden/rider) + # rider Text editor in rust +## Build + +```bash +curl https://sh.rustup.rs -sSf | sh +sudo 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 +rustup run nightly cargo build +``` + ## Road map ### v1.0 @@ -15,10 +26,10 @@ Text editor in rust * [ ] `Save file` with shortcut * [ ] `Save file as...` with shortcut * [x] Theme based menu UI -* [ ] Lock scroll when no available content +* [x] Lock scroll when no available content * [ ] Config edit menu * [ ] Project tree -* [ ] Cover `rider` with tests at least 50% +* [x] Cover `rider` with tests at least 50% * [x] Handle resize window * [ ] Selection diff --git a/src/app/app_state.rs b/src/app/app_state.rs index 0bdf3be..dc0b98d 100644 --- a/src/app/app_state.rs +++ b/src/app/app_state.rs @@ -57,9 +57,11 @@ impl AppState { } impl Render for AppState { - 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); + fn render(&self, canvas: &mut WC, renderer: &mut Renderer, _context: &RenderContext) { + self.file_editor + .render(canvas, renderer, &RenderContext::Nothing); + self.menu_bar + .render(canvas, renderer, &RenderContext::Nothing); } fn prepare_ui(&mut self, renderer: &mut Renderer) { diff --git a/src/app/application.rs b/src/app/application.rs index 1ecdeb1..246e96a 100644 --- a/src/app/application.rs +++ b/src/app/application.rs @@ -147,7 +147,7 @@ impl Application { app_state.file_editor_mut().move_caret(MoveDirection::Down); } UpdateResult::Scroll { x, y } => { - app_state.file_editor_mut().scroll_to(x, y); + app_state.file_editor_mut().scroll_to(-x, -y); } UpdateResult::WindowResize { width, height } => { let mut c = app_state.config().write().unwrap(); @@ -172,7 +172,7 @@ impl Application { self.clear(); app_state.update(timer.ticks() as i32, &UpdateContext::Nothing); - app_state.render(&mut self.canvas, &mut renderer, None); + app_state.render(&mut self.canvas, &mut renderer, &RenderContext::Nothing); self.present(); sleep(sleep_time); diff --git a/src/config/config.rs b/src/config/config.rs index bb2a3f4..3c11652 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -1,5 +1,6 @@ use crate::config::creator; use crate::config::EditorConfig; +use crate::config::ScrollConfig; use crate::lexer::Language; use crate::themes::Theme; use dirs; @@ -12,11 +13,11 @@ pub type LanguageMapping = HashMap; pub struct Config { width: u32, height: u32, - scroll_speed: i32, menu_height: u16, editor_config: EditorConfig, theme: Theme, extensions_mapping: LanguageMapping, + scroll: ScrollConfig, } impl Config { @@ -31,18 +32,14 @@ impl Config { Self { width: 1024, height: 860, - scroll_speed: 10, menu_height: 60, theme: Theme::load(editor_config.current_theme().clone()), editor_config, extensions_mapping, + scroll: ScrollConfig::new(), } } - pub fn scroll_speed(&self) -> i32 { - self.scroll_speed - } - pub fn width(&self) -> u32 { self.width } @@ -82,6 +79,14 @@ impl Config { pub fn extensions_mapping(&self) -> &LanguageMapping { &self.extensions_mapping } + + pub fn scroll(&self) -> &ScrollConfig { + &self.scroll + } + + pub fn scroll_mut(&mut self) -> &mut ScrollConfig { + &mut self.scroll + } } #[cfg(test)] @@ -111,4 +116,23 @@ mod tests { assert_eq!(keys, expected); } } + + #[test] + fn assert_scroll() { + let config = Config::new(); + let result = config.scroll(); + let expected = ScrollConfig::new(); + assert_eq!(result.clone(), expected); + } + + #[test] + fn assert_scroll_mut() { + let mut config = Config::new(); + let result = config.scroll_mut(); + result.set_margin_right(1236); + let mut expected = ScrollConfig::new(); + expected.set_margin_right(1236); + assert_eq!(result.clone(), expected); + } + } diff --git a/src/config/mod.rs b/src/config/mod.rs index 7b7514b..35a7e4a 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -4,10 +4,12 @@ pub mod config; pub(crate) mod creator; pub mod directories; pub mod editor_config; +pub mod scroll_config; pub use crate::config::config::*; pub use crate::config::directories::*; pub use crate::config::editor_config::*; +pub use crate::config::scroll_config::*; pub type ConfigAccess = Arc>; diff --git a/src/config/scroll_config.rs b/src/config/scroll_config.rs new file mode 100644 index 0000000..9669b4a --- /dev/null +++ b/src/config/scroll_config.rs @@ -0,0 +1,96 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ScrollConfig { + width: u32, + margin_right: i32, + speed: i32, +} + +impl ScrollConfig { + pub fn new() -> Self { + Self { + width: 4, + margin_right: 5, + speed: 10, + } + } + + pub fn width(&self) -> u32 { + self.width + } + + pub fn set_width(&mut self, width: u32) { + self.width = width; + } + + pub fn margin_right(&self) -> i32 { + self.margin_right + } + + pub fn set_margin_right(&mut self, margin_right: i32) { + self.margin_right = margin_right; + } + + pub fn speed(&self) -> i32 { + self.speed + } + + pub fn set_speed(&mut self, speed: i32) { + self.speed = speed + } +} + +mod tests { + use super::*; + + #[test] + fn assert_width() { + let config = ScrollConfig::new(); + let result = config.width(); + let expected = 4; + assert_eq!(result, expected); + } + + #[test] + fn assert_set_width() { + let mut config = ScrollConfig::new(); + config.set_width(60); + let result = config.width(); + let expected = 60; + assert_eq!(result, expected); + } + + #[test] + fn assert_margin_right() { + let config = ScrollConfig::new(); + let result = config.margin_right(); + let expected = 5; + assert_eq!(result, expected); + } + + #[test] + fn assert_set_margin_right() { + let mut config = ScrollConfig::new(); + config.set_margin_right(98); + let result = config.margin_right(); + let expected = 98; + assert_eq!(result, expected); + } + + #[test] + fn assert_speed() { + let config = ScrollConfig::new(); + let result = config.speed(); + let expected = 10; + assert_eq!(result, expected); + } + + #[test] + fn assert_set_speed() { + let mut config = ScrollConfig::new(); + config.set_speed(98); + let result = config.speed(); + let expected = 98; + assert_eq!(result, expected); + } + +} diff --git a/src/ui/caret/caret.rs b/src/ui/caret/caret.rs index 919fa52..fe627b3 100644 --- a/src/ui/caret/caret.rs +++ b/src/ui/caret/caret.rs @@ -71,10 +71,13 @@ impl Deref for Caret { } 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(), + fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, context: &RenderContext) { + use std::borrow::*; + use std::option::*; + + let dest = match context.borrow() { + RenderContext::RelativePosition(p) => move_render_point(p.clone(), self.dest()), + _ => self.dest().clone(), }; let start = Point::new(dest.x(), dest.y()); let end = Point::new(dest.x(), dest.y() + dest.height() as i32); diff --git a/src/ui/file/editor_file.rs b/src/ui/file/editor_file.rs index e6c9a96..51f9d2e 100644 --- a/src/ui/file/editor_file.rs +++ b/src/ui/file/editor_file.rs @@ -32,17 +32,11 @@ impl EditorFile { ext, Arc::clone(&config), )]; - let render_position = { - let c = config.read().unwrap(); - let x = c.editor_left_margin(); - let y = c.editor_top_margin(); - Rect::new(x, y, 0, 0) - }; Self { path, sections, - dest: render_position, + dest: Rect::new(0, 0, 0, 0), buffer, config, line_height: 0, @@ -69,7 +63,20 @@ impl EditorFile { &self.dest } - pub fn get_character_at(&self, index: usize) -> Option { + pub fn get_section_at_mut(&mut self, index: usize) -> Option<&mut EditorFileSection> { + self.sections.get_mut(index) + } + + fn refresh_characters_position(&mut self) { + let mut current: Rect = Rect::new(0, 0, 0, 0); + for section in self.sections.iter_mut() { + section.update_positions(&mut current); + } + } +} + +impl TextCollection for EditorFile { + fn get_character_at(&self, index: usize) -> Option { for section in self.sections.iter() { let character = section.get_character_at(index); if character.is_some() { @@ -79,7 +86,7 @@ impl EditorFile { None } - pub fn get_line(&self, line: &usize) -> Option> { + fn get_line(&self, line: &usize) -> Option> { let mut vec: Vec<&TextCharacter> = vec![]; for section in self.sections.iter() { match section.get_line(line) { @@ -95,7 +102,7 @@ impl EditorFile { } } - pub fn get_last_at_line(&self, line: usize) -> Option { + fn get_last_at_line(&self, line: usize) -> Option { let mut current = None; for section in self.sections.iter() { let c = section.get_last_at_line(line); @@ -105,23 +112,12 @@ impl EditorFile { } current } - - pub fn get_section_at_mut(&mut self, index: usize) -> Option<&mut EditorFileSection> { - self.sections.get_mut(index) - } - - fn refresh_characters_position(&mut self) { - let mut current: Rect = Rect::new(0, 0, 0, 0); - for section in self.sections.iter_mut() { - section.update_positions(&mut current); - } - } } impl Render for EditorFile { - fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) { + fn render(&self, canvas: &mut WC, renderer: &mut Renderer, context: &RenderContext) { for section in self.sections.iter() { - section.render(canvas, renderer, parent); + section.render(canvas, renderer, context); } } @@ -156,7 +152,6 @@ impl ClickHandler for EditorFile { } } if index >= 0 { - let context = UpdateContext::ParentPosition(self.render_start_point()); return self .get_section_at_mut(index as usize) .unwrap() @@ -165,10 +160,9 @@ impl ClickHandler for EditorFile { UR::NoOp } - fn is_left_click_target(&self, point: &Point, _context: &UpdateContext) -> bool { - let context = UpdateContext::ParentPosition(self.render_start_point()); + fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool { for section in self.sections.iter() { - if section.is_left_click_target(point, &context) { + if section.is_left_click_target(point, context) { return true; } } @@ -176,6 +170,27 @@ impl ClickHandler for EditorFile { } } +impl TextWidget for EditorFile { + fn full_rect(&self) -> Rect { + let mut max_line_width = 0; + let mut height = 0; + for (index, section) in self.sections.iter().enumerate() { + let r = section.full_rect(); + + if index == 0 { + height = r.height(); + max_line_width = r.width(); + } else { + height += r.height(); + if max_line_width < r.width() { + max_line_width = r.width(); + } + } + } + Rect::new(0, 0, max_line_width, height) + } +} + impl RenderBox for EditorFile { fn render_start_point(&self) -> Point { self.dest.top_left() @@ -185,3 +200,37 @@ impl RenderBox for EditorFile { &self.dest } } + +#[cfg(test)] +mod test_render_box { + use crate::app::*; + use crate::tests::support; + use crate::ui::*; + use sdl2::rect::*; + use sdl2::*; + use std::borrow::*; + use std::rc::*; + use std::sync::*; + + #[test] + fn assert_dest() { + let config = support::build_config(); + let buffer = "".to_owned(); + let path = "/example.txt".to_owned(); + let widget = EditorFile::new(path, buffer, config); + let result = widget.dest().clone(); + let expected = Rect::new(0, 0, 1, 1); + assert_eq!(result, expected); + } + + #[test] + fn assert_render_start_point() { + let config = support::build_config(); + let buffer = "".to_owned(); + let path = "/example.txt".to_owned(); + let widget = EditorFile::new(path, buffer, config); + let result = widget.render_start_point().clone(); + let expected = Point::new(0, 0); + assert_eq!(result, expected); + } +} diff --git a/src/ui/file/editor_file_section.rs b/src/ui/file/editor_file_section.rs index dd1c225..a2e24a4 100644 --- a/src/ui/file/editor_file_section.rs +++ b/src/ui/file/editor_file_section.rs @@ -59,8 +59,36 @@ impl EditorFileSection { c.update_position(current); } } +} - pub fn get_character_at(&self, index: usize) -> Option { +impl TextWidget for EditorFileSection { + fn full_rect(&self) -> Rect { + let mut current_line_width = 0; + let mut max_line_width = 0; + let mut height = 0; + for (index, token) in self.tokens.iter().enumerate() { + let r = token.full_rect(); + + if index == 0 { + height = r.height(); + current_line_width = r.width(); + max_line_width = r.width(); + } else if token.is_new_line() { + height += r.height(); + if max_line_width < current_line_width { + max_line_width = current_line_width; + } + current_line_width = 0; + } else { + current_line_width += r.width(); + } + } + Rect::new(0, 0, max_line_width, height) + } +} + +impl TextCollection for EditorFileSection { + fn get_character_at(&self, index: usize) -> Option { for token in self.tokens.iter() { let character = token.get_character_at(index); if character.is_some() { @@ -70,7 +98,7 @@ impl EditorFileSection { None } - pub fn get_line(&self, line: &usize) -> Option> { + fn get_line(&self, line: &usize) -> Option> { let mut vec: Vec<&TextCharacter> = vec![]; for token in self.tokens.iter() { match token.get_line(line) { @@ -85,7 +113,7 @@ impl EditorFileSection { } } - pub fn get_last_at_line(&self, line: usize) -> Option { + fn get_last_at_line(&self, line: usize) -> Option { let mut current: Option = None; for token in self.tokens.iter() { if !token.is_last_in_line() { @@ -101,9 +129,9 @@ impl EditorFileSection { } impl Render for EditorFileSection { - fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) { + fn render(&self, canvas: &mut WC, renderer: &mut Renderer, context: &RenderContext) { for token in self.tokens.iter() { - token.render(canvas, renderer, parent); + token.render(canvas, renderer, context); } } diff --git a/src/ui/file/editor_file_token.rs b/src/ui/file/editor_file_token.rs index 4d244ee..3625e4d 100644 --- a/src/ui/file/editor_file_token.rs +++ b/src/ui/file/editor_file_token.rs @@ -50,13 +50,35 @@ impl EditorFileToken { self.last_in_line } + pub fn is_new_line(&self) -> bool { + self.token_type.is_new_line() + } + pub fn update_position(&mut self, current: &mut Rect) { for text_character in self.characters.iter_mut() { text_character.update_position(current); } } +} - pub fn get_character_at(&self, index: usize) -> Option { +impl TextWidget for EditorFileToken { + fn full_rect(&self) -> Rect { + let mut rect = Rect::new(0, 0, 0, 0); + match self.characters.first() { + Some(c) => { + rect.set_x(c.dest().x()); + rect.set_y(c.dest().y()); + rect.set_width(c.dest().width()); + rect.set_height(c.dest().height()); + } + _ => return rect, + }; + rect + } +} + +impl TextCollection for EditorFileToken { + fn get_character_at(&self, index: usize) -> Option { for character in self.characters.iter() { if character.position() == index { return Some(character.clone()); @@ -65,7 +87,7 @@ impl EditorFileToken { None } - pub fn get_line(&self, line: &usize) -> Option> { + fn get_line(&self, line: &usize) -> Option> { let mut vec: Vec<&TextCharacter> = vec![]; for c in self.characters.iter() { match ( @@ -93,7 +115,7 @@ impl EditorFileToken { } } - pub fn get_last_at_line(&self, line: usize) -> Option { + fn get_last_at_line(&self, line: usize) -> Option { let mut current: Option<&TextCharacter> = None; for text_character in self.characters.iter() { if !text_character.is_last_in_line() { @@ -112,12 +134,12 @@ 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) { + fn render(&self, canvas: &mut WC, renderer: &mut Renderer, context: &RenderContext) { if self.token_type.is_new_line() { return; } for text_character in self.characters.iter() { - text_character.render(canvas, renderer, parent); + text_character.render(canvas, renderer, context); } } diff --git a/src/ui/file/mod.rs b/src/ui/file/mod.rs index 28b1c2b..df19731 100644 --- a/src/ui/file/mod.rs +++ b/src/ui/file/mod.rs @@ -1,3 +1,5 @@ +use sdl2::rect::Rect; + pub mod editor_file; pub mod editor_file_section; pub mod editor_file_token; @@ -5,3 +7,16 @@ pub mod editor_file_token; pub use crate::ui::file::editor_file::*; pub use crate::ui::file::editor_file_section::*; pub use crate::ui::file::editor_file_token::*; +use crate::ui::TextCharacter; + +pub trait TextCollection { + fn get_character_at(&self, index: usize) -> Option; + + fn get_line(&self, line: &usize) -> Option>; + + fn get_last_at_line(&self, line: usize) -> Option; +} + +pub trait TextWidget { + fn full_rect(&self) -> Rect; +} diff --git a/src/ui/file_editor.rs b/src/ui/file_editor.rs deleted file mode 100644 index 8e5322e..0000000 --- a/src/ui/file_editor.rs +++ /dev/null @@ -1,266 +0,0 @@ -use sdl2::rect::*; -use std::borrow::*; -use std::mem; -use std::rc::Rc; -use std::sync::*; - -use crate::app::*; -use crate::app::{UpdateResult as UR, WindowCanvas as WS}; -use crate::config::*; -use crate::ui::*; - -pub struct FileEditor { - dest: Rect, - scroll: Point, - caret: Caret, - file: Option, - config: ConfigAccess, -} - -impl FileEditor { - pub fn new(config: ConfigAccess) -> Self { - let dest = { - let c = config.read().unwrap(); - Rect::new( - c.editor_left_margin(), - c.editor_top_margin(), - c.width() - c.editor_left_margin() as u32, - c.height() - c.editor_top_margin() as u32, - ) - }; - Self { - dest, - scroll: Point::new(0, 0), - caret: Caret::new(Arc::clone(&config)), - file: None, - config, - } - } - - pub fn caret(&self) -> &Caret { - &self.caret - } - - pub fn caret_mut(&mut self) -> &mut Caret { - &mut self.caret - } - - pub fn has_file(&self) -> bool { - self.file.is_some() - } - - pub fn drop_file(&mut self) -> Option { - if self.has_file() { - let mut file = None; - mem::swap(&mut self.file, &mut file); - file - } else { - None - } - } - - pub fn open_file(&mut self, file: EditorFile) -> Option { - let mut file = Some(file); - mem::swap(&mut self.file, &mut file); - file - } - - pub fn file(&self) -> Option<&EditorFile> { - self.file.as_ref() - } - - pub fn file_mut(&mut self) -> Option<&mut EditorFile> { - self.file.as_mut() - } - - pub fn move_caret(&mut self, dir: MoveDirection) { - match dir { - MoveDirection::Left => {} - MoveDirection::Right => caret_manager::move_caret_right(self), - MoveDirection::Up => {} - MoveDirection::Down => {} - } - } - - pub fn delete_front(&mut self, renderer: &mut Renderer) { - file_content_manager::delete_front(self, renderer); - } - - pub fn delete_back(&mut self, renderer: &mut Renderer) { - file_content_manager::delete_back(self, renderer); - } - - pub fn insert_text(&mut self, text: String, renderer: &mut Renderer) { - file_content_manager::insert_text(self, text, renderer); - } - - pub fn insert_new_line(&mut self, renderer: &mut Renderer) { - file_content_manager::insert_new_line(self, renderer); - } - - pub fn replace_current_file(&mut self, file: EditorFile) { - self.open_file(file); - } - - pub fn scroll_to(&mut self, x: i32, y: i32) { - let read_config = self.config.read().unwrap(); - self.scroll = self.scroll - + Point::new( - read_config.scroll_speed() * x, - read_config.scroll_speed() * y, - ); - } - - fn is_text_character_clicked(&self, point: &Point) -> bool { - let context = UpdateContext::ParentPosition(self.render_start_point()); - self.file() - .map_or(false, |file| file.is_left_click_target(point, &context)) - } - - fn is_editor_clicked(&self, point: &Point) -> bool { - self.dest - .contains_point(move_render_point(point.clone(), &self.dest).top_left()) - } - - fn resolve_line_from_point(&self, point: &Point) -> i32 { - let file = match self.file() { - Some(f) => f, - _ => return 0, - }; - let mut y = point.y() - self.render_start_point().y(); - if y < 0 { - y = 0; - } - y / (file.line_height() as i32) - } - - fn set_caret_to_end_of_line(&mut self, line: i32) { - let file = match self.file_mut() { - Some(f) => f, - _ => return, - }; - let mut line = line; - while line >= 0 { - match file.get_last_at_line(line.clone() as usize) { - Some(text_character) => { - let rect = text_character.dest(); - let position = - CaretPosition::new(text_character.position() + 1, line as usize, 0); - let p = if text_character.is_last_in_line() && text_character.is_new_line() { - rect.top_left() - } else { - rect.top_right() - }; - self.caret.move_caret(position, p); - break; - } - _ => { - line -= 1; - } - } - } - } -} - -impl Render for FileEditor { - 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)), - _ => (), - }; - self.caret.render(canvas, renderer, Some(self)); - } - - fn prepare_ui(&mut self, renderer: &mut Renderer) { - self.caret.prepare_ui(renderer); - } -} - -impl Update for FileEditor { - fn update(&mut self, ticks: i32, context: &UpdateContext) -> UR { - { - let config = self.config.read().unwrap(); - self.dest - .set_width(config.width() - config.editor_left_margin() as u32); - self.dest - .set_height(config.height() - config.editor_top_margin() as u32); - } - self.caret.update(ticks, context); - match self.file_mut() { - Some(file) => file.update(ticks, context), - _ => UR::NoOp, - } - } -} - -impl ClickHandler for FileEditor { - fn on_left_click(&mut self, point: &Point, _context: &UpdateContext) -> UR { - let context = UpdateContext::ParentPosition(self.render_start_point()); - - if self.is_text_character_clicked(point) { - let file = if let Some(file) = self.file_mut() { - file - } else { - return UR::NoOp; - }; - match file.on_left_click(point, &context) { - UR::MoveCaret(rect, position) => { - self.caret - .move_caret(position, Point::new(rect.x(), rect.y())); - } - _ => {} - } - } else { - self.set_caret_to_end_of_line(self.resolve_line_from_point(point)); - } - UR::NoOp - } - - fn is_left_click_target(&self, point: &Point, _context: &UpdateContext) -> bool { - self.is_text_character_clicked(point) || self.is_editor_clicked(point) - } -} - -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)] -mod tests { - use crate::app::*; - use crate::ui::*; - use sdl2::rect::*; - use sdl2::*; - use std::borrow::*; - use std::rc::*; - use std::sync::*; - - #[test] - fn replace_file() { - let config = Arc::new(RwLock::new(Config::new())); - let mut editor = FileEditor::new(Arc::clone(&config)); - let first_file = - EditorFile::new("./foo.txt".to_string(), "foo".to_string(), config.clone()); - let second_file = - EditorFile::new("./bar.txt".to_string(), "bar".to_string(), config.clone()); - editor.open_file(first_file.clone()); - let result = editor.open_file(second_file.clone()); - assert_eq!(result.is_some(), true); - let file = result.as_ref().unwrap(); - assert_eq!(file.path(), first_file.path()); - assert_eq!(file.buffer(), first_file.buffer()); - } -} diff --git a/src/ui/file_editor/file_editor.rs b/src/ui/file_editor/file_editor.rs new file mode 100644 index 0000000..3d5f1e0 --- /dev/null +++ b/src/ui/file_editor/file_editor.rs @@ -0,0 +1,429 @@ +use sdl2::pixels::*; +use sdl2::rect::*; +use std::borrow::*; +use std::mem; +use std::sync::*; + +use crate::app::*; +use crate::app::{UpdateResult as UR, WindowCanvas as WS}; +use crate::ui::scroll_bar::horizontal_scroll_bar::*; +use crate::ui::scroll_bar::vertical_scroll_bar::*; +use crate::ui::scroll_bar::Scrollable; +use crate::ui::*; + +pub struct FileEditor { + dest: Rect, + full_rect: Rect, + caret: Caret, + file: Option, + config: ConfigAccess, + vertical_scroll_bar: VerticalScrollBar, + horizontal_scroll_bar: HorizontalScrollBar, +} + +impl FileEditor { + pub fn new(config: ConfigAccess) -> Self { + let dest = { + let c = config.read().unwrap(); + Rect::new( + c.editor_left_margin(), + c.editor_top_margin(), + c.width() - c.editor_left_margin() as u32, + c.height() - c.editor_top_margin() as u32, + ) + }; + Self { + dest, + full_rect: Rect::new(0, 0, 0, 0), + caret: Caret::new(Arc::clone(&config)), + vertical_scroll_bar: VerticalScrollBar::new(Arc::clone(&config)), + horizontal_scroll_bar: HorizontalScrollBar::new(Arc::clone(&config)), + file: None, + config, + } + } + + pub fn delete_front(&mut self, renderer: &mut Renderer) { + file_content_manager::delete_front(self, renderer); + } + + pub fn delete_back(&mut self, renderer: &mut Renderer) { + file_content_manager::delete_back(self, renderer); + } + + pub fn insert_text(&mut self, text: String, renderer: &mut Renderer) { + file_content_manager::insert_text(self, text, renderer); + } + + pub fn insert_new_line(&mut self, renderer: &mut Renderer) { + file_content_manager::insert_new_line(self, renderer); + } + + fn is_text_character_clicked(&self, point: &Point) -> bool { + let context = UpdateContext::ParentPosition(self.render_start_point()); + self.file() + .map_or(false, |file| file.is_left_click_target(point, &context)) + } + + fn is_editor_clicked(&self, point: &Point) -> bool { + self.dest + .contains_point(move_render_point(point.clone(), &self.dest).top_left()) + } + + fn resolve_line_from_point(&self, point: &Point) -> i32 { + let file = match self.file() { + Some(f) => f, + _ => return 0, + }; + let mut y = point.y() - self.render_start_point().y(); + if y < 0 { + y = 0; + } + y / (file.line_height() as i32) + } +} + +impl ScrollableView for FileEditor { + fn scroll_to(&mut self, x: i32, y: i32) { + let read_config = self.config.read().unwrap(); + + let value_x = read_config.scroll().speed() * x; + let value_y = read_config.scroll().speed() * y; + let old_x = self.horizontal_scroll_bar.scroll_value(); + let old_y = self.vertical_scroll_bar.scroll_value(); + + if value_x + old_x >= 0 { + self.horizontal_scroll_bar.scroll_to(value_x + old_x); + if self.horizontal_scroll_bar.scrolled_part() > 1.0 { + self.horizontal_scroll_bar.scroll_to(old_x); + } + } + if value_y + old_y >= 0 { + self.vertical_scroll_bar.scroll_to(value_y + old_y); + if self.vertical_scroll_bar.scrolled_part() > 1.0 { + self.vertical_scroll_bar.scroll_to(old_y); + } + } + } + + fn scroll(&self) -> Point { + Point::new( + -self.horizontal_scroll_bar.scroll_value(), + -self.vertical_scroll_bar.scroll_value(), + ) + } +} + +impl FileAccess for FileEditor { + fn has_file(&self) -> bool { + self.file.is_some() + } + + fn file(&self) -> Option<&EditorFile> { + self.file.as_ref() + } + + fn file_mut(&mut self) -> Option<&mut EditorFile> { + self.file.as_mut() + } + + fn open_file(&mut self, file: EditorFile) -> Option { + let mut file = Some(file); + mem::swap(&mut self.file, &mut file); + if let Some(f) = self.file.as_ref() { + self.full_rect = f.full_rect(); + } + file + } + + fn drop_file(&mut self) -> Option { + if self.has_file() { + let mut file = None; + mem::swap(&mut self.file, &mut file); + file + } else { + None + } + } + + fn replace_current_file(&mut self, file: EditorFile) { + self.open_file(file); + } +} + +impl CaretAccess for FileEditor { + fn caret(&self) -> &Caret { + &self.caret + } + + fn caret_mut(&mut self) -> &mut Caret { + &mut self.caret + } + + fn move_caret(&mut self, dir: MoveDirection) { + match dir { + MoveDirection::Left => {} + MoveDirection::Right => caret_manager::move_caret_right(self), + MoveDirection::Up => {} + MoveDirection::Down => {} + } + } + + fn set_caret_to_end_of_line(&mut self, line: i32) { + let file = match self.file_mut() { + Some(f) => f, + _ => return, + }; + let mut line = line; + while line >= 0 { + match file.get_last_at_line(line.clone() as usize) { + Some(text_character) => { + let rect = text_character.dest(); + let position = + CaretPosition::new(text_character.position() + 1, line as usize, 0); + let p = if text_character.is_last_in_line() && text_character.is_new_line() { + rect.top_left() + } else { + rect.top_right() + }; + self.caret.move_caret(position, p); + break; + } + _ => { + line -= 1; + } + } + } + } +} + +impl Render for FileEditor { + fn render(&self, canvas: &mut WS, renderer: &mut Renderer, _context: &RenderContext) { + canvas.set_clip_rect(self.dest.clone()); + match self.file() { + Some(file) => file.render( + canvas, + renderer, + &RenderContext::RelativePosition(self.render_start_point()), + ), + _ => (), + }; + self.caret.render( + canvas, + renderer, + &RenderContext::RelativePosition(self.render_start_point()), + ); + self.vertical_scroll_bar.render( + canvas, + renderer, + &RenderContext::RelativePosition(self.dest.top_left()), + ); + self.horizontal_scroll_bar.render( + canvas, + renderer, + &RenderContext::RelativePosition(self.dest.top_left()), + ); + } + + fn prepare_ui(&mut self, renderer: &mut Renderer) { + self.caret.prepare_ui(renderer); + } +} + +impl Update for FileEditor { + fn update(&mut self, ticks: i32, context: &UpdateContext) -> UR { + let (width, height, editor_left_margin, editor_top_margin, scroll_width, scroll_margin) = { + let config: RwLockReadGuard = self.config.read().unwrap(); + ( + config.width(), + config.height(), + config.editor_left_margin() as u32, + config.editor_top_margin() as u32, + config.scroll().width(), + config.scroll().margin_right(), + ) + }; + self.dest.set_width(width - editor_left_margin); + self.dest.set_height(height - editor_top_margin); + + self.vertical_scroll_bar + .set_full_size(self.full_rect.height()); + self.vertical_scroll_bar.set_viewport(self.dest.height()); + self.vertical_scroll_bar + .set_location(self.dest.width() as i32 - (scroll_width as i32 + scroll_margin)); + self.vertical_scroll_bar.update(ticks, context); + + self.horizontal_scroll_bar + .set_full_size(self.full_rect.width()); + self.horizontal_scroll_bar.set_viewport(self.dest.width()); + self.horizontal_scroll_bar + .set_location(self.dest.height() as i32 - (scroll_width as i32 + scroll_margin)); + self.horizontal_scroll_bar.update(ticks, context); + + self.caret.update(ticks, context); + match self.file_mut() { + Some(file) => file.update(ticks, context), + _ => UR::NoOp, + } + } +} + +impl ClickHandler for FileEditor { + fn on_left_click(&mut self, point: &Point, _context: &UpdateContext) -> UR { + let context = UpdateContext::ParentPosition(self.render_start_point()); + + if self.is_text_character_clicked(point) { + let file = if let Some(file) = self.file_mut() { + file + } else { + return UR::NoOp; + }; + match file.on_left_click(point, &context) { + UR::MoveCaret(rect, position) => { + self.caret + .move_caret(position, Point::new(rect.x(), rect.y())); + } + _ => {} + } + } else { + self.set_caret_to_end_of_line(self.resolve_line_from_point(point)); + } + UR::NoOp + } + + fn is_left_click_target(&self, point: &Point, _context: &UpdateContext) -> bool { + self.is_text_character_clicked(point) || self.is_editor_clicked(point) + } +} + +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)] +mod tests { + use crate::app::*; + use crate::ui::*; + use sdl2::rect::*; + use sdl2::*; + use std::borrow::*; + use std::rc::*; + use std::sync::*; + + #[test] + fn replace_file() { + let config = Arc::new(RwLock::new(Config::new())); + let mut editor = FileEditor::new(Arc::clone(&config)); + let first_file = + EditorFile::new("./foo.txt".to_string(), "foo".to_string(), config.clone()); + let second_file = + EditorFile::new("./bar.txt".to_string(), "bar".to_string(), config.clone()); + editor.open_file(first_file.clone()); + let result = editor.open_file(second_file.clone()); + assert_eq!(result.is_some(), true); + let file = result.as_ref().unwrap(); + assert_eq!(file.path(), first_file.path()); + assert_eq!(file.buffer(), first_file.buffer()); + } +} + +#[cfg(test)] +mod test_config_holder { + use crate::app::*; + use crate::tests::support; + use crate::ui::*; + use sdl2::rect::*; + use sdl2::*; + use std::borrow::*; + use std::rc::*; + use std::sync::*; + + #[test] + fn assert_config() { + let config = support::build_config(); + let widget = FileEditor::new(Arc::clone(&config)); + let result = widget.config(); + { + let mut w = config.write().unwrap(); + w.set_height(1240); + w.set_width(1024); + } + let local = config.read().unwrap(); + let widget_config = result.read().unwrap(); + assert_eq!(widget_config.width(), local.width()); + assert_eq!(widget_config.height(), local.height()); + } +} + +#[cfg(test)] +mod test_render_box { + use crate::app::*; + use crate::tests::support; + use crate::ui::*; + use sdl2::rect::*; + use sdl2::*; + use std::borrow::*; + use std::rc::*; + use std::sync::*; + + impl FileEditor { + pub fn set_full_rect(&mut self, r: Rect) { + self.full_rect = r; + } + + pub fn set_dest(&mut self, r: Rect) { + self.dest = r; + } + } + + #[test] + fn assert_dest() { + let config = support::build_config(); + let (x, y, mw, mh) = { + let c = config.read().unwrap(); + ( + c.editor_left_margin(), + c.editor_top_margin(), + c.width(), + c.height(), + ) + }; + let widget = FileEditor::new(config); + let result = widget.dest().clone(); + let expected = Rect::new(x, y, mw - x as u32, mh - y as u32); + assert_eq!(result, expected); + } + + #[test] + fn assert_render_start_point() { + let config = support::build_config(); + let (x, y, ss) = { + let c = config.read().unwrap(); + ( + c.editor_left_margin(), + c.editor_top_margin(), + c.scroll().speed(), + ) + }; + let mut widget = FileEditor::new(config); + widget.set_dest(Rect::new(x.clone(), y.clone(), 999, 999)); + widget.set_full_rect(Rect::new(0, 0, 99999, 99999)); + widget.update(1, &UpdateContext::Nothing); + widget.scroll_to(30, 40); + let result = widget.render_start_point().clone(); + let expected = Point::new(x - (ss * 30), y - (ss * 40)); + assert_eq!(result, expected); + } +} diff --git a/src/ui/file_editor/mod.rs b/src/ui/file_editor/mod.rs new file mode 100644 index 0000000..5ed267c --- /dev/null +++ b/src/ui/file_editor/mod.rs @@ -0,0 +1,36 @@ +use crate::ui::*; +use sdl2::rect::*; + +pub mod file_editor; + +pub use crate::ui::file_editor::file_editor::*; + +pub trait FileAccess { + fn has_file(&self) -> bool; + + fn file(&self) -> Option<&EditorFile>; + + fn file_mut(&mut self) -> Option<&mut EditorFile>; + + fn open_file(&mut self, file: EditorFile) -> Option; + + fn drop_file(&mut self) -> Option; + + fn replace_current_file(&mut self, file: EditorFile); +} + +pub trait CaretAccess { + fn caret(&self) -> &Caret; + + fn caret_mut(&mut self) -> &mut Caret; + + fn move_caret(&mut self, dir: MoveDirection); + + fn set_caret_to_end_of_line(&mut self, line: i32); +} + +pub trait ScrollableView { + fn scroll_to(&mut self, x: i32, y: i32); + + fn scroll(&self) -> Point; +} diff --git a/src/ui/menu_bar.rs b/src/ui/menu_bar.rs index 0529807..7c3b45f 100644 --- a/src/ui/menu_bar.rs +++ b/src/ui/menu_bar.rs @@ -34,21 +34,23 @@ impl MenuBar { } impl Render for MenuBar { - fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, parent: Parent) { + fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, context: &RenderContext) { + use std::borrow::*; + canvas.set_clip_rect(self.dest.clone()); canvas.set_draw_color(self.background_color.clone()); canvas - .fill_rect(match parent { - None => self.dest.clone(), - Some(parent) => move_render_point(parent.render_start_point(), self.dest()), + .fill_rect(match context.borrow() { + RenderContext::RelativePosition(p) => move_render_point(p.clone(), self.dest()), + _ => self.dest.clone(), }) .unwrap_or_else(|_| panic!("Failed to draw main menu background")); canvas.set_draw_color(self.border_color.clone()); canvas - .draw_rect(match parent { - None => self.dest.clone(), - Some(parent) => move_render_point(parent.render_start_point(), self.dest()), + .draw_rect(match context.borrow() { + RenderContext::RelativePosition(p) => move_render_point(p.clone(), self.dest()), + _ => self.dest.clone(), }) .unwrap_or_else(|_| panic!("Failed to draw main menu background")); } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index ffeaa9b..bbfe59f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -10,6 +10,7 @@ pub mod file; pub mod file_editor; pub mod menu_bar; pub mod project_tree; +pub mod scroll_bar; pub mod text_character; pub use crate::ui::caret::*; @@ -17,17 +18,22 @@ pub use crate::ui::file::*; pub use crate::ui::file_editor::*; pub use crate::ui::menu_bar::*; pub use crate::ui::project_tree::*; +pub use crate::ui::scroll_bar::*; pub use crate::ui::text_character::*; -pub type Parent<'l> = Option<&'l RenderBox>; -pub type ParentMut<'l> = Option<&'l mut RenderBox>; - +#[derive(Debug)] pub enum UpdateContext<'l> { Nothing, ParentPosition(Point), CurrentFile(&'l mut EditorFile), } +#[derive(Clone, PartialEq, Debug)] +pub enum RenderContext { + Nothing, + RelativePosition(Point), +} + #[inline] pub fn is_in_rect(point: &Point, rect: &Rect) -> bool { rect.contains_point(point.clone()) @@ -64,7 +70,7 @@ pub fn move_render_point(p: Point, d: &Rect) -> Rect { } pub trait Render { - fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent); + fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: &RenderContext); fn prepare_ui(&mut self, renderer: &mut Renderer); } diff --git a/src/ui/scroll_bar/horizontal_scroll_bar.rs b/src/ui/scroll_bar/horizontal_scroll_bar.rs new file mode 100644 index 0000000..d06da6f --- /dev/null +++ b/src/ui/scroll_bar/horizontal_scroll_bar.rs @@ -0,0 +1,203 @@ +use crate::app::{UpdateResult as UR, WindowCanvas as WC}; +use crate::config::*; +use crate::renderer::*; +use crate::ui::*; +use sdl2::pixels::*; +use sdl2::rect::*; + +pub struct HorizontalScrollBar { + scroll_value: i32, + viewport: u32, + full_width: u32, + rect: Rect, +} + +impl HorizontalScrollBar { + pub fn new(config: ConfigAccess) -> Self { + let width = { config.read().unwrap().scroll().width() }; + Self { + scroll_value: 0, + viewport: 1, + full_width: 1, + rect: Rect::new(0, 0, width, 0), + } + } + + #[inline] + pub fn viewport(&self) -> u32 { + self.viewport + } + + #[inline] + pub fn full_width(&self) -> u32 { + self.full_width + } + + #[inline] + pub fn rect(&self) -> &Rect { + &self.rect + } +} + +impl Update for HorizontalScrollBar { + fn update(&mut self, _ticks: i32, _context: &UpdateContext) -> UR { + if self.full_width < self.viewport { + return UR::NoOp; + } + let ratio = self.full_width as f64 / self.viewport as f64; + self.rect.set_width((self.viewport as f64 / ratio) as u32); + let x = (self.viewport - self.rect.width()) as f64 + * (self.scroll_value().abs() as f64 / (self.full_width - self.viewport) as f64); + self.rect.set_x(x as i32); + + UR::NoOp + } +} + +impl Render for HorizontalScrollBar { + fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, context: &RenderContext) { + if self.full_width < self.viewport { + return; + } + + canvas.set_draw_color(Color::RGBA(255, 255, 255, 0)); + canvas + .fill_rect(match context { + RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.rect), + _ => self.rect.clone(), + }) + .unwrap_or_else(|_| panic!("Failed to render vertical scroll back")); + } + + fn prepare_ui(&mut self, _renderer: &mut Renderer) {} +} + +impl Scrollable for HorizontalScrollBar { + fn scroll_to(&mut self, n: i32) { + self.scroll_value = n; + } + + fn scroll_value(&self) -> i32 { + self.scroll_value + } + + fn set_viewport(&mut self, n: u32) { + self.viewport = n; + } + + fn set_full_size(&mut self, n: u32) { + self.full_width = n; + } + + fn set_location(&mut self, n: i32) { + self.rect.set_y(n); + } + + fn scrolled_part(&self) -> f64 { + if self.full_width() < self.viewport() { + return 1.0; + } + self.scroll_value().abs() as f64 / (self.full_width() - self.viewport()) as f64 + } +} + +#[cfg(test)] +mod test_update { + use super::*; + use crate::tests::support; + use std::sync::*; + + impl HorizontalScrollBar { + pub fn rect_mut(&mut self) -> &mut Rect { + &mut self.rect + } + } + + #[test] + fn assert_do_nothing_when_small_content() { + let config = support::build_config(); + let mut widget = HorizontalScrollBar::new(Arc::clone(&config)); + widget.set_viewport(100); + widget.set_full_size(20); + widget.rect_mut().set_x(30000000); + widget.rect_mut().set_width(30000000); + widget.update(0, &UpdateContext::Nothing); + assert_eq!(widget.rect().x(), 30000000); + assert_eq!(widget.rect().width(), 30000000); + } + + #[test] + fn assert_update_when_huge_content() { + let config = support::build_config(); + let mut widget = HorizontalScrollBar::new(Arc::clone(&config)); + widget.set_viewport(100); + widget.set_full_size(200); + widget.rect_mut().set_x(30000000); + widget.rect_mut().set_width(30000000); + widget.update(0, &UpdateContext::Nothing); + assert_eq!(widget.rect().x(), 0); + assert_eq!(widget.rect().width(), 50); + } +} + +#[cfg(test)] +mod test_scrollable { + use super::*; + use crate::tests::support; + use std::sync::*; + + #[test] + fn assert_scroll_to() { + let config = support::build_config(); + let mut widget = HorizontalScrollBar::new(Arc::clone(&config)); + let old = widget.scroll_value(); + widget.scroll_to(157); + let current = widget.scroll_value(); + let expected = 157; + assert_ne!(old, current); + assert_eq!(current, expected); + } + + #[test] + fn assert_scroll_value() { + let config = support::build_config(); + let widget = HorizontalScrollBar::new(Arc::clone(&config)); + assert_eq!(widget.scroll_value(), 0); + } + + #[test] + fn assert_set_viewport() { + let config = support::build_config(); + let mut widget = HorizontalScrollBar::new(Arc::clone(&config)); + let old = widget.viewport(); + widget.set_viewport(157); + let current = widget.viewport(); + let expected = 157; + assert_ne!(old, current); + assert_eq!(current, expected); + } + + #[test] + fn assert_set_full_size() { + let config = support::build_config(); + let mut widget = HorizontalScrollBar::new(Arc::clone(&config)); + let old = widget.full_width(); + widget.set_full_size(157); + let current = widget.full_width(); + let expected = 157; + assert_ne!(old, current); + assert_eq!(current, expected); + } + + #[test] + fn assert_set_location() { + let config = support::build_config(); + let mut widget = HorizontalScrollBar::new(Arc::clone(&config)); + let old = widget.rect().y(); + widget.set_location(157); + let current = widget.rect().y(); + let expected = 157; + assert_ne!(old, current); + assert_eq!(current, expected); + } +} diff --git a/src/ui/scroll_bar/mod.rs b/src/ui/scroll_bar/mod.rs new file mode 100644 index 0000000..3ab32b1 --- /dev/null +++ b/src/ui/scroll_bar/mod.rs @@ -0,0 +1,19 @@ +pub mod horizontal_scroll_bar; +pub mod vertical_scroll_bar; + +use crate::ui::scroll_bar::horizontal_scroll_bar::*; +use crate::ui::scroll_bar::vertical_scroll_bar::*; + +pub trait Scrollable { + fn scroll_to(&mut self, n: i32); + + fn scroll_value(&self) -> i32; + + fn set_viewport(&mut self, n: u32); + + fn set_full_size(&mut self, n: u32); + + fn set_location(&mut self, n: i32); + + fn scrolled_part(&self) -> f64; +} diff --git a/src/ui/scroll_bar/vertical_scroll_bar.rs b/src/ui/scroll_bar/vertical_scroll_bar.rs new file mode 100644 index 0000000..bf51e5e --- /dev/null +++ b/src/ui/scroll_bar/vertical_scroll_bar.rs @@ -0,0 +1,200 @@ +use crate::app::{UpdateResult as UR, WindowCanvas as WC}; +use crate::config::*; +use crate::renderer::*; +use crate::ui::*; +use sdl2::pixels::*; +use sdl2::rect::*; + +pub struct VerticalScrollBar { + scroll_value: i32, + viewport: u32, + full_height: u32, + rect: Rect, +} + +impl VerticalScrollBar { + pub fn new(config: ConfigAccess) -> Self { + let width = { config.read().unwrap().scroll().width() }; + Self { + scroll_value: 0, + viewport: 1, + full_height: 1, + rect: Rect::new(0, 0, width, 0), + } + } + + #[inline] + pub fn viewport(&self) -> u32 { + self.viewport + } + + #[inline] + pub fn full_height(&self) -> u32 { + self.full_height + } + + #[inline] + pub fn rect(&self) -> &Rect { + &self.rect + } +} + +impl Update for VerticalScrollBar { + fn update(&mut self, _ticks: i32, _context: &UpdateContext) -> UR { + if self.full_height() < self.viewport() { + return UR::NoOp; + } + let ratio = self.full_height() as f64 / self.viewport() as f64; + self.rect + .set_height((self.viewport() as f64 / ratio) as u32); + let y = (self.viewport() - self.rect.height()) as f64 * self.scrolled_part(); + self.rect.set_y(y as i32); + + UR::NoOp + } +} + +impl Render for VerticalScrollBar { + fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, context: &RenderContext) { + if self.full_height() < self.viewport() { + return; + } + + canvas.set_draw_color(Color::RGBA(255, 255, 255, 0)); + canvas + .fill_rect(match context { + RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.rect), + _ => self.rect.clone(), + }) + .unwrap_or_else(|_| panic!("Failed to render vertical scroll back")); + } + + fn prepare_ui(&mut self, _renderer: &mut Renderer) {} +} + +impl Scrollable for VerticalScrollBar { + fn scroll_to(&mut self, n: i32) { + self.scroll_value = n; + } + + fn scroll_value(&self) -> i32 { + self.scroll_value + } + + fn set_viewport(&mut self, n: u32) { + self.viewport = n; + } + + fn set_full_size(&mut self, n: u32) { + self.full_height = n; + } + + fn set_location(&mut self, n: i32) { + self.rect.set_x(n); + } + + fn scrolled_part(&self) -> f64 { + self.scroll_value().abs() as f64 / (self.full_height() - self.viewport()) as f64 + } +} + +#[cfg(test)] +mod test_update { + use super::*; + use crate::tests::support; + use std::sync::*; + + impl VerticalScrollBar { + pub fn rect_mut(&mut self) -> &mut Rect { + &mut self.rect + } + } + + #[test] + fn assert_do_nothing_when_small_content() { + let config = support::build_config(); + let mut widget = VerticalScrollBar::new(Arc::clone(&config)); + widget.set_viewport(100); + widget.set_full_size(20); + widget.rect_mut().set_y(30000000); + widget.rect_mut().set_height(30000000); + widget.update(0, &UpdateContext::Nothing); + assert_eq!(widget.rect().y(), 30000000); + assert_eq!(widget.rect().height(), 30000000); + } + + #[test] + fn assert_update_when_huge_content() { + let config = support::build_config(); + let mut widget = VerticalScrollBar::new(Arc::clone(&config)); + widget.set_viewport(100); + widget.set_full_size(200); + widget.rect_mut().set_y(30000000); + widget.rect_mut().set_height(30000000); + widget.update(0, &UpdateContext::Nothing); + assert_eq!(widget.rect().y(), 0); + assert_eq!(widget.rect().height(), 50); + } +} + +#[cfg(test)] +mod test_scrollable { + use super::*; + use crate::tests::support; + use std::sync::*; + + #[test] + fn assert_scroll_to() { + let config = support::build_config(); + let mut widget = VerticalScrollBar::new(Arc::clone(&config)); + let old = widget.scroll_value(); + widget.scroll_to(157); + let current = widget.scroll_value(); + let expected = 157; + assert_ne!(old, current); + assert_eq!(current, expected); + } + + #[test] + fn assert_scroll_value() { + let config = support::build_config(); + let widget = VerticalScrollBar::new(Arc::clone(&config)); + assert_eq!(widget.scroll_value(), 0); + } + + #[test] + fn assert_set_viewport() { + let config = support::build_config(); + let mut widget = VerticalScrollBar::new(Arc::clone(&config)); + let old = widget.viewport(); + widget.set_viewport(157); + let current = widget.viewport(); + let expected = 157; + assert_ne!(old, current); + assert_eq!(current, expected); + } + + #[test] + fn assert_set_full_size() { + let config = support::build_config(); + let mut widget = VerticalScrollBar::new(Arc::clone(&config)); + let old = widget.full_height(); + widget.set_full_size(157); + let current = widget.full_height(); + let expected = 157; + assert_ne!(old, current); + assert_eq!(current, expected); + } + + #[test] + fn assert_set_location() { + let config = support::build_config(); + let mut widget = VerticalScrollBar::new(Arc::clone(&config)); + let old = widget.rect().x(); + widget.set_location(157); + let current = widget.rect().x(); + let expected = 157; + assert_ne!(old, current); + assert_eq!(current, expected); + } +} diff --git a/src/ui/text_character.rs b/src/ui/text_character.rs index e626cc0..c445f6d 100644 --- a/src/ui/text_character.rs +++ b/src/ui/text_character.rs @@ -105,7 +105,7 @@ 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) { + fn render(&self, canvas: &mut WC, renderer: &mut Renderer, context: &RenderContext) { if self.is_new_line() { return; } @@ -126,9 +126,9 @@ impl Render for TextCharacter { color: self.color.clone(), font: font_details.clone(), }; - let dest = match parent { - None => self.dest.clone(), - Some(parent) => move_render_point(parent.render_start_point(), self.dest()), + let dest = match context { + RenderContext::RelativePosition(p) => move_render_point(p.clone(), self.dest()), + _ => self.dest.clone(), }; if let Ok(texture) = renderer.texture_manager().load_text(&mut details, &font) { canvas