diff --git a/README.md b/README.md index 899b209..626e072 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Text editor in rust ```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 +rustup run nightly cargo build --all -rr ``` ## Road map diff --git a/rider-config/src/scroll_config.rs b/rider-config/src/scroll_config.rs index 3f4149b..1c3059a 100644 --- a/rider-config/src/scroll_config.rs +++ b/rider-config/src/scroll_config.rs @@ -10,7 +10,7 @@ impl ScrollConfig { Self { width: 4, margin_right: 5, - speed: 10, + speed: 30, } } @@ -91,7 +91,7 @@ mod tests { fn assert_speed() { let config = ScrollConfig::new(); let result = config.speed(); - let expected = 10; + let expected = 30; assert_eq!(result, expected); } diff --git a/rider-editor/src/app/app_state.rs b/rider-editor/src/app/app_state.rs index fcdc06e..62304d8 100644 --- a/rider-editor/src/app/app_state.rs +++ b/rider-editor/src/app/app_state.rs @@ -1,4 +1,6 @@ -use crate::app::{UpdateResult, WindowCanvas as WC}; +use crate::app::application::Application; +use crate::app::UpdateResult; +use crate::renderer::renderer::Renderer; use crate::renderer::CanvasRenderer; use crate::ui::*; use rider_config::*; @@ -9,6 +11,7 @@ use std::sync::*; pub struct AppState { menu_bar: MenuBar, + project_tree: ProjectTreeSidebar, files: Vec, config: Arc>, file_editor: FileEditor, @@ -19,6 +22,10 @@ impl AppState { pub fn new(config: Arc>) -> Self { Self { menu_bar: MenuBar::new(Arc::clone(&config)), + project_tree: ProjectTreeSidebar::new( + Application::current_working_directory(), + config.clone(), + ), files: vec![], file_editor: FileEditor::new(Arc::clone(&config)), open_file_modal: None, @@ -40,18 +47,22 @@ impl AppState { }; } - #[cfg_attr(tarpaulin, skip)] - pub fn open_directory(&mut self, dir_path: String, renderer: &mut CanvasRenderer) { + pub fn open_directory(&mut self, dir_path: String, renderer: &mut R) + where + R: Renderer + CharacterSizeManager, + { match self.open_file_modal.as_mut() { Some(modal) => modal.open_directory(dir_path, renderer), - _ => (), + None => self.project_tree.open_directory(dir_path, renderer), }; } + #[cfg_attr(tarpaulin, skip)] pub fn file_editor(&self) -> &FileEditor { &self.file_editor } + #[cfg_attr(tarpaulin, skip)] pub fn file_editor_mut(&mut self) -> &mut FileEditor { &mut self.file_editor } @@ -75,23 +86,38 @@ impl AppState { #[cfg_attr(tarpaulin, skip)] impl AppState { - pub fn render(&self, canvas: &mut WC, renderer: &mut CanvasRenderer, _context: &RenderContext) { + pub fn render(&self, canvas: &mut C, renderer: &mut R, _context: &RenderContext) + where + C: CanvasAccess, + R: Renderer + ConfigHolder + CharacterSizeManager, + { + // file editor self.file_editor.render(canvas, renderer); + + // menu bar self.menu_bar.render(canvas, &RenderContext::Nothing); + + // project tree + self.project_tree.render(canvas, renderer); + + // open file modal match self.open_file_modal.as_ref() { Some(modal) => modal.render(canvas, renderer, &RenderContext::Nothing), _ => (), }; } - pub fn prepare_ui(&mut self, renderer: &mut CanvasRenderer) { + pub fn prepare_ui(&mut self, renderer: &mut R) + where + R: Renderer + CharacterSizeManager, + { self.menu_bar.prepare_ui(); + self.project_tree.prepare_ui(renderer); self.file_editor.prepare_ui(renderer); } -} -impl Update for AppState { - fn update(&mut self, ticks: i32, context: &UpdateContext) -> UpdateResult { + pub fn update(&mut self, ticks: i32, context: &UpdateContext) -> UpdateResult { + // open file modal let res = match self.open_file_modal.as_mut() { Some(modal) => modal.update(ticks, &UpdateContext::Nothing), _ => UpdateResult::NoOp, @@ -100,8 +126,17 @@ impl Update for AppState { return res; } + // menu bar self.menu_bar.update(ticks, context); - self.file_editor.update(ticks, context); + + // sidebar + self.project_tree.update(ticks); + + // file editor + let context = UpdateContext::ParentPosition( + self.project_tree.full_rect().top_right() + Point::new(10, 0), + ); + self.file_editor.update(ticks, &context); UpdateResult::NoOp } } @@ -109,6 +144,14 @@ impl Update for AppState { impl AppState { #[cfg_attr(tarpaulin, skip)] pub fn on_left_click(&mut self, point: &Point, video_subsystem: &mut VS) -> UpdateResult { + if self + .project_tree + .is_left_click_target(point, &UpdateContext::Nothing) + { + return self + .project_tree + .on_left_click(point, &UpdateContext::Nothing); + } match self.open_file_modal.as_mut() { Some(modal) => return modal.on_left_click(point, &UpdateContext::Nothing), _ => (), diff --git a/rider-editor/src/app/application.rs b/rider-editor/src/app/application.rs index 593ce6f..cb6a634 100644 --- a/rider-editor/src/app/application.rs +++ b/rider-editor/src/app/application.rs @@ -16,6 +16,7 @@ use sdl2::surface::Surface; use sdl2::video::Window; use sdl2::EventPump; use sdl2::{Sdl, TimerSubsystem, VideoSubsystem}; +use std::env; use std::process::Command; use std::sync::{Arc, RwLock}; use std::thread::sleep; @@ -29,6 +30,8 @@ pub enum UpdateResult { Stop, RefreshPositions, MouseLeftClicked(Point), + MouseDragStart(Point), + MouseDragStop(Point), MoveCaret(Rect, CaretPosition), DeleteFront, DeleteBack, @@ -44,6 +47,7 @@ pub enum UpdateResult { OpenFile(String), OpenDirectory(String), OpenFileModal, + FileDropped(String), } #[cfg_attr(tarpaulin, skip)] @@ -187,14 +191,16 @@ impl Application { app_state.open_directory(dir_path.clone(), &mut renderer); } UpdateResult::OpenFileModal => { - use std::env; - let pwd = env::current_dir().unwrap().to_str().unwrap().to_string(); + let pwd = Self::current_working_directory(); let mut modal = OpenFile::new(pwd.clone(), 400, 800, Arc::clone(&self.config)); modal.prepare_ui(&mut renderer); modal.open_directory(pwd.clone(), &mut renderer); app_state.set_open_file_modal(Some(modal)); } + UpdateResult::MouseDragStart(_point) => (), + UpdateResult::MouseDragStop(_point) => (), + UpdateResult::FileDropped(_path) => (), } } self.tasks = new_tasks; @@ -241,80 +247,78 @@ impl Application { Event::Quit { .. } => self.tasks.push(UpdateResult::Stop), Event::MouseButtonUp { mouse_btn, x, y, .. - } => match mouse_btn { - MouseButton::Left => self - .tasks - .push(UpdateResult::MouseLeftClicked(Point::new(x, y))), - _ => (), - }, - Event::KeyDown { keycode, .. } => { - let keycode = if keycode.is_some() { - keycode.unwrap() - } else { - continue; - }; - - match keycode { - Keycode::Backspace => { - self.tasks.push(UpdateResult::DeleteFront); - } - Keycode::Delete => { - self.tasks.push(UpdateResult::DeleteBack); - } - Keycode::KpEnter | Keycode::Return => { - self.tasks.push(UpdateResult::InsertNewLine); - } - Keycode::Left => { - self.tasks.push(UpdateResult::MoveCaretLeft); - } - Keycode::Right => { - self.tasks.push(UpdateResult::MoveCaretRight); - } - Keycode::Up => { - self.tasks.push(UpdateResult::MoveCaretUp); - } - Keycode::Down => { - self.tasks.push(UpdateResult::MoveCaretDown); - } - Keycode::O => { - if left_control_pressed && !shift_pressed { - self.tasks.push(UpdateResult::OpenFileModal); - } - } - _ => {} - }; + } if mouse_btn == MouseButton::Left => { + self.tasks + .push(UpdateResult::MouseDragStart(Point::new(x, y))); + self.tasks + .push(UpdateResult::MouseLeftClicked(Point::new(x, y))); } + Event::DropFile { filename, .. } => { + self.tasks.push(UpdateResult::FileDropped(filename)) + } + Event::MouseButtonDown { + x, y, mouse_btn, .. + } if mouse_btn == MouseButton::Left => self + .tasks + .push(UpdateResult::MouseDragStart(Point::new(x, y))), + Event::KeyDown { keycode, .. } if keycode.is_some() => match keycode.unwrap() { + Keycode::Backspace => { + self.tasks.push(UpdateResult::DeleteFront); + } + Keycode::Delete => { + self.tasks.push(UpdateResult::DeleteBack); + } + Keycode::KpEnter | Keycode::Return => { + self.tasks.push(UpdateResult::InsertNewLine); + } + Keycode::Left => { + self.tasks.push(UpdateResult::MoveCaretLeft); + } + Keycode::Right => { + self.tasks.push(UpdateResult::MoveCaretRight); + } + Keycode::Up => { + self.tasks.push(UpdateResult::MoveCaretUp); + } + Keycode::Down => { + self.tasks.push(UpdateResult::MoveCaretDown); + } + Keycode::O if left_control_pressed && !shift_pressed => { + self.tasks.push(UpdateResult::OpenFileModal) + } + _ => {} + }, Event::TextInput { text, .. } => { self.tasks.push(UpdateResult::Input(text)); } Event::MouseWheel { direction, x, y, .. - } => { - match direction { - MouseWheelDirection::Normal => { - self.tasks.push(UpdateResult::Scroll { x, y }); - } - MouseWheelDirection::Flipped => { - self.tasks.push(UpdateResult::Scroll { x, y: -y }); - } - _ => { - // ignore - } - }; - } + } => match direction { + MouseWheelDirection::Normal => { + self.tasks.push(UpdateResult::Scroll { x, y }); + } + MouseWheelDirection::Flipped => { + self.tasks.push(UpdateResult::Scroll { x, y: -y }); + } + _ => { + // ignore + } + }, Event::Window { win_event: WindowEvent::Resized(w, h), .. - } => { - self.tasks.push(UpdateResult::WindowResize { - width: w, - height: h, - }); - } + } => self.tasks.push(UpdateResult::WindowResize { + width: w, + height: h, + }), _ => {} } } } + + pub fn current_working_directory() -> String { + env::current_dir().unwrap().to_str().unwrap().to_string() + } } #[cfg_attr(tarpaulin, skip)] diff --git a/rider-editor/src/app/caret_manager.rs b/rider-editor/src/app/caret_manager.rs index 3012bf7..5e225db 100644 --- a/rider-editor/src/app/caret_manager.rs +++ b/rider-editor/src/app/caret_manager.rs @@ -26,16 +26,18 @@ pub fn move_caret_left(file_editor: &mut FileEditor) { if file_editor.caret().text_position() == 0 { return; } - let c: TextCharacter = match file.get_character_at(file_editor.caret().text_position() - 1) { - Some(text_character) => text_character, - None => return, // EOF - }; + let text_character: TextCharacter = + match file.get_character_at(file_editor.caret().text_position() - 1) { + Some(text_character) => text_character, + None => return, // EOF + }; let pos = file_editor.caret().position(); - let d = c.dest().clone(); + let character_destination = text_character.dest().clone(); let p = pos.moved(-1, 0, 0); - file_editor - .caret_mut() - .move_caret(p, Point::new(d.x(), d.y())); + file_editor.caret_mut().move_caret( + p, + Point::new(character_destination.x(), character_destination.y()), + ); } #[cfg(test)] @@ -77,6 +79,11 @@ mod test_move_right { ) -> Result, String> { Err("skip render character".to_owned()) } + + #[cfg_attr(tarpaulin, skip)] + fn load_image(&mut self, _path: String) -> Result, String> { + unimplemented!() + } } impl ConfigHolder for RendererMock { @@ -163,6 +170,11 @@ mod test_move_left { unimplemented!() } + #[cfg_attr(tarpaulin, skip)] + fn load_image(&mut self, _path: String) -> Result, String> { + unimplemented!() + } + fn load_text_tex( &mut self, _details: &mut TextDetails, diff --git a/rider-editor/src/app/file_content_manager.rs b/rider-editor/src/app/file_content_manager.rs index d73c062..d0d8ebb 100644 --- a/rider-editor/src/app/file_content_manager.rs +++ b/rider-editor/src/app/file_content_manager.rs @@ -15,12 +15,10 @@ pub fn delete_front(file_editor: &mut FileEditor, renderer: &mut R) where R: ConfigHolder + CharacterSizeManager + Renderer, { - let mut buffer: String = if let Some(file) = file_editor.file() { - file - } else { - return; - } - .buffer(); + let mut buffer: String = match file_editor.file() { + Some(file) => file.buffer(), + _ => return, + }; let position: CaretPosition = file_editor.caret().position().clone(); if position.text_position() == 0 { return; @@ -28,20 +26,16 @@ where let c: char = buffer.chars().collect::>()[position.text_position() - 1]; buffer.remove(position.text_position() - 1); let position = match c { - '\n' if position.text_position() > 0 && position.line_number() > 0 => { - position.moved(-1, -1, 0) - } + '\n' if !position.is_first() => position.moved(-1, -1, 0), '\n' => position.clone(), _ if position.text_position() > 0 => position.moved(-1, 0, 0), _ => position.moved(0, 0, 0), }; let move_to = file_editor .file() - .and_then(|f| f.get_character_at(file_editor.caret().text_position())) - .and_then(|character| { - let dest: Rect = character.dest(); - Some((position, Point::new(dest.x(), dest.y()))) - }); + .and_then(|f| f.get_character_at(position.text_position())) + .map(|character| character.dest()) + .map(|dest| (position, Point::new(dest.x(), dest.y()))); match move_to { Some((position, point)) => file_editor.caret_mut().move_caret(position, point), None => file_editor.caret_mut().reset_caret(), @@ -60,10 +54,9 @@ pub fn delete_back(file_editor: &mut FileEditor, renderer: &mut R) where R: ConfigHolder + CharacterSizeManager + Renderer, { - let file: &EditorFile = if let Some(file) = file_editor.file() { - file - } else { - return; + let file: &EditorFile = match file_editor.file() { + Some(file) => file, + None => return, }; let mut buffer: String = file.buffer(); let position: usize = file_editor.caret().text_position(); @@ -178,6 +171,11 @@ mod tests { ) -> Result, String> { Err("skip render character".to_owned()) } + + #[cfg_attr(tarpaulin, skip)] + fn load_image(&mut self, _path: String) -> Result, String> { + unimplemented!() + } } impl ConfigHolder for RendererMock { diff --git a/rider-editor/src/app/mod.rs b/rider-editor/src/app/mod.rs index c4a9e64..6138e19 100644 --- a/rider-editor/src/app/mod.rs +++ b/rider-editor/src/app/mod.rs @@ -7,3 +7,7 @@ pub use crate::app::app_state::*; pub use crate::app::application::*; pub use crate::app::caret_manager::*; pub use crate::app::file_content_manager::*; + +pub trait Resize { + fn resize_element(&mut self); +} diff --git a/rider-editor/src/renderer/renderer.rs b/rider-editor/src/renderer/renderer.rs index 8eab64a..998104f 100644 --- a/rider-editor/src/renderer/renderer.rs +++ b/rider-editor/src/renderer/renderer.rs @@ -18,6 +18,8 @@ pub trait Renderer { details: &mut TextDetails, font_details: FontDetails, ) -> Result, String>; + + fn load_image(&mut self, path: String) -> Result, String>; } #[cfg_attr(tarpaulin, skip)] @@ -117,4 +119,8 @@ impl<'l> Renderer for CanvasRenderer<'l> { let tex_manager = self.texture_manager(); tex_manager.load_text(details, font) } + + fn load_image(&mut self, path: String) -> Result, String> { + self.texture_manager.load(path.as_str()) + } } diff --git a/rider-editor/src/tests.rs b/rider-editor/src/tests.rs index 345bdcc..a23ef96 100644 --- a/rider-editor/src/tests.rs +++ b/rider-editor/src/tests.rs @@ -1,11 +1,159 @@ #[cfg(test)] pub mod support { + use crate::renderer::managers::FontDetails; + use crate::renderer::managers::TextDetails; + use crate::renderer::renderer::Renderer; + use crate::ui::text_character::CharacterSizeManager; + use crate::ui::CanvasAccess; use rider_config::Config; + use rider_config::ConfigAccess; + use rider_config::ConfigHolder; + use sdl2::pixels::Color; + use sdl2::rect::Point; + use sdl2::rect::Rect; + use sdl2::render::Texture; + use sdl2::ttf::Font; + use std::fmt::Debug; + use std::fmt::Error; + use std::fmt::Formatter; + use std::rc::Rc; use std::sync::*; + pub fn build_path(path: String) { + use std::fs; + + fs::create_dir_all(path.as_str()).unwrap(); + fs::write((path.clone() + &"/file1".to_owned()).as_str(), "foo").unwrap(); + fs::write((path.clone() + &"/file2".to_owned()).as_str(), "bar").unwrap(); + fs::create_dir_all((path.clone() + &"/dir1".to_owned()).as_str()).unwrap(); + fs::create_dir_all((path.clone() + &"/dir2".to_owned()).as_str()).unwrap(); + } + pub fn build_config() -> Arc> { let mut config = Config::new(); config.set_theme(config.editor_config().current_theme().clone()); Arc::new(RwLock::new(config)) } + + #[derive(Debug, PartialEq)] + pub struct RendererRect { + pub rect: Rect, + pub color: Color, + } + + #[cfg_attr(tarpaulin, skip)] + pub struct CanvasMock { + pub rects: Vec, + pub borders: Vec, + pub lines: Vec, + pub clippings: Vec, + } + + #[cfg_attr(tarpaulin, skip)] + impl Debug for CanvasMock { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "CanvasMock {{}}") + } + } + + #[cfg_attr(tarpaulin, skip)] + impl PartialEq for CanvasMock { + fn eq(&self, other: &CanvasMock) -> bool { + self.rects == other.rects + && self.borders == other.borders + && self.clippings == other.clippings + && self.lines == other.lines + } + } + + #[cfg_attr(tarpaulin, skip)] + impl CanvasMock { + pub fn new() -> Self { + Self { + rects: vec![], + borders: vec![], + lines: vec![], + clippings: vec![], + } + } + } + + #[cfg_attr(tarpaulin, skip)] + impl CanvasAccess for CanvasMock { + fn render_rect(&mut self, rect: Rect, color: Color) -> Result<(), String> { + self.rects.push(RendererRect { rect, color }); + Ok(()) + } + + fn render_border(&mut self, rect: Rect, color: Color) -> Result<(), String> { + self.borders.push(RendererRect { rect, color }); + Ok(()) + } + + fn render_image( + &mut self, + _tex: Rc, + _src: Rect, + _dest: Rect, + ) -> Result<(), String> { + unimplemented!() + } + + fn render_line(&mut self, start: Point, end: Point, color: Color) -> Result<(), String> { + self.lines.push(RendererRect { + rect: Rect::new(start.x(), start.y(), end.x() as u32, end.y() as u32), + color, + }); + Ok(()) + } + + fn set_clipping(&mut self, rect: Rect) { + self.clippings.push(rect); + } + } + + #[cfg_attr(tarpaulin, skip)] + pub struct SimpleRendererMock { + config: ConfigAccess, + } + + #[cfg_attr(tarpaulin, skip)] + impl SimpleRendererMock { + pub fn new(config: ConfigAccess) -> Self { + Self { config } + } + } + + #[cfg_attr(tarpaulin, skip)] + impl Renderer for SimpleRendererMock { + fn load_font(&mut self, _details: FontDetails) -> Rc { + unimplemented!() + } + + fn load_text_tex( + &mut self, + _details: &mut TextDetails, + _font_details: FontDetails, + ) -> Result, String> { + Err("skip text texture".to_owned()) + } + + fn load_image(&mut self, _path: String) -> Result, String> { + Err("skip img texture".to_owned()) + } + } + + #[cfg_attr(tarpaulin, skip)] + impl CharacterSizeManager for SimpleRendererMock { + fn load_character_size(&mut self, _c: char) -> Rect { + Rect::new(0, 0, 13, 14) + } + } + + #[cfg_attr(tarpaulin, skip)] + impl ConfigHolder for SimpleRendererMock { + fn config(&self) -> &Arc> { + &self.config + } + } } diff --git a/rider-editor/src/ui/caret/caret.rs b/rider-editor/src/ui/caret/caret.rs index e673bba..192c687 100644 --- a/rider-editor/src/ui/caret/caret.rs +++ b/rider-editor/src/ui/caret/caret.rs @@ -99,7 +99,7 @@ impl Caret { impl Caret { pub fn update(&mut self) -> UR { self.blink_delay += 1; - if self.blink_delay >= 30 { + if self.blink_delay >= 15 { self.blink_delay = 0; self.toggle_state(); } diff --git a/rider-editor/src/ui/caret/caret_position.rs b/rider-editor/src/ui/caret/caret_position.rs index d82a025..9219b39 100644 --- a/rider-editor/src/ui/caret/caret_position.rs +++ b/rider-editor/src/ui/caret/caret_position.rs @@ -14,32 +14,39 @@ impl CaretPosition { } } + #[inline] pub fn text_position(&self) -> usize { self.text_position } + #[inline] pub fn line_number(&self) -> usize { self.line_number } + #[inline] pub fn line_position(&self) -> usize { self.line_position } + #[inline] pub fn reset(&mut self) { self.text_position = 0; self.line_number = 0; self.line_position = 0; } + #[inline] pub fn set_text_position(&mut self, n: usize) { self.text_position = n; } + #[inline] pub fn set_line_number(&mut self, n: usize) { self.line_number = n; } + #[inline] pub fn set_line_position(&mut self, n: usize) { self.line_position = n; } @@ -51,6 +58,11 @@ impl CaretPosition { line_position: (self.line_position as i32 + line_position) as usize, } } + + #[inline] + pub fn is_first(&self) -> bool { + self.line_number == 0 && self.text_position == 0 + } } #[cfg(test)] diff --git a/rider-editor/src/ui/file/editor_file_token.rs b/rider-editor/src/ui/file/editor_file_token.rs index a6f5ff7..60fe7c4 100644 --- a/rider-editor/src/ui/file/editor_file_token.rs +++ b/rider-editor/src/ui/file/editor_file_token.rs @@ -200,6 +200,7 @@ impl ClickHandler for EditorFileToken { mod tests { use super::*; use crate::tests::support::build_config; + use crate::tests::support::CanvasMock; use rider_lexers::Token; use sdl2::pixels::PixelFormatEnum; use sdl2::render::Texture; @@ -207,9 +208,6 @@ mod tests { use sdl2::surface::Surface; use sdl2::surface::SurfaceContext; use sdl2::ttf::Font; - use std::fmt::Debug; - use std::fmt::Error; - use std::fmt::Formatter; use std::rc::Rc; use std::sync::{Arc, RwLock}; @@ -217,83 +215,6 @@ mod tests { // models //################################################## - #[derive(Debug, PartialEq)] - struct RendererRect { - pub rect: Rect, - pub color: Color, - } - - #[cfg_attr(tarpaulin, skip)] - struct CanvasMock { - pub rects: Vec, - pub borders: Vec, - pub lines: Vec, - pub clippings: Vec, - } - - #[cfg_attr(tarpaulin, skip)] - impl Debug for CanvasMock { - fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { - write!(f, "CanvasMock {{}}") - } - } - - #[cfg_attr(tarpaulin, skip)] - impl PartialEq for CanvasMock { - fn eq(&self, other: &CanvasMock) -> bool { - self.rects == other.rects - && self.borders == other.borders - && self.clippings == other.clippings - && self.lines == other.lines - } - } - - #[cfg_attr(tarpaulin, skip)] - impl CanvasMock { - pub fn new() -> Self { - Self { - rects: vec![], - borders: vec![], - lines: vec![], - clippings: vec![], - } - } - } - - #[cfg_attr(tarpaulin, skip)] - impl CanvasAccess for CanvasMock { - fn render_rect(&mut self, rect: Rect, color: Color) -> Result<(), String> { - self.rects.push(RendererRect { rect, color }); - Ok(()) - } - - fn render_border(&mut self, rect: Rect, color: Color) -> Result<(), String> { - self.borders.push(RendererRect { rect, color }); - Ok(()) - } - - fn render_image( - &mut self, - _tex: Rc, - _src: Rect, - _dest: Rect, - ) -> Result<(), String> { - unimplemented!() - } - - fn render_line(&mut self, start: Point, end: Point, color: Color) -> Result<(), String> { - self.lines.push(RendererRect { - rect: Rect::new(start.x(), start.y(), end.x() as u32, end.y() as u32), - color, - }); - Ok(()) - } - - fn set_clipping(&mut self, rect: Rect) { - self.clippings.push(rect); - } - } - #[cfg_attr(tarpaulin, skip)] struct RendererMock<'l> { pub config: Arc>, @@ -317,10 +238,16 @@ mod tests { #[cfg_attr(tarpaulin, skip)] impl<'l> Renderer for RendererMock<'l> { + #[cfg_attr(tarpaulin, skip)] fn load_font(&mut self, _details: FontDetails) -> Rc { unimplemented!("load_font") } + #[cfg_attr(tarpaulin, skip)] + fn load_image(&mut self, _path: String) -> Result, String> { + unimplemented!() + } + fn load_text_tex( &mut self, _details: &mut TextDetails, @@ -367,6 +294,7 @@ mod tests { let mut token = EditorFileToken::new(&token_type, false, config.clone()); token.prepare_ui(&mut renderer); } + #[test] fn assert_keyword_to_color() { let config = build_config(); @@ -378,6 +306,7 @@ mod tests { let mut token = EditorFileToken::new(&token_type, false, config.clone()); token.prepare_ui(&mut renderer); } + #[test] fn assert_string_to_color() { let config = build_config(); @@ -389,6 +318,7 @@ mod tests { let mut token = EditorFileToken::new(&token_type, false, config.clone()); token.prepare_ui(&mut renderer); } + #[test] fn assert_identifier_to_color() { let config = build_config(); @@ -400,6 +330,7 @@ mod tests { let mut token = EditorFileToken::new(&token_type, false, config.clone()); token.prepare_ui(&mut renderer); } + #[test] fn assert_literal_to_color() { let config = build_config(); @@ -411,6 +342,7 @@ mod tests { let mut token = EditorFileToken::new(&token_type, false, config.clone()); token.prepare_ui(&mut renderer); } + #[test] fn assert_comment_to_color() { let config = build_config(); @@ -422,6 +354,7 @@ mod tests { let mut token = EditorFileToken::new(&token_type, false, config.clone()); token.prepare_ui(&mut renderer); } + #[test] fn assert_operator_to_color() { let config = build_config(); @@ -433,6 +366,7 @@ mod tests { let mut token = EditorFileToken::new(&token_type, false, config.clone()); token.prepare_ui(&mut renderer); } + #[test] fn assert_separator_to_color() { let config = build_config(); diff --git a/rider-editor/src/ui/file_editor/file_editor.rs b/rider-editor/src/ui/file_editor/file_editor.rs deleted file mode 100644 index 1b3cfeb..0000000 --- a/rider-editor/src/ui/file_editor/file_editor.rs +++ /dev/null @@ -1,432 +0,0 @@ -use crate::app::UpdateResult as UR; -use crate::app::*; -use crate::renderer::renderer::Renderer; -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::*; -use sdl2::rect::{Point, Rect}; -use std::mem; -use std::sync::*; - -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 R) - where - R: ConfigHolder + CharacterSizeManager + Renderer, - { - file_content_manager::delete_front(self, renderer); - } - - pub fn delete_back(&mut self, renderer: &mut R) - where - R: ConfigHolder + CharacterSizeManager + Renderer, - { - file_content_manager::delete_back(self, renderer); - } - - pub fn insert_text(&mut self, text: String, renderer: &mut R) - where - R: ConfigHolder + CharacterSizeManager + Renderer, - { - file_content_manager::insert_text(self, text, renderer); - } - - pub fn insert_new_line(&mut self, renderer: &mut R) - where - R: ConfigHolder + CharacterSizeManager + 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_by(&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(); - } - self.vertical_scroll_bar.set_location(0); - self.horizontal_scroll_bar.set_location(0); - 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 => caret_manager::move_caret_left(self), - 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 FileEditor { - pub fn render(&self, canvas: &mut C, renderer: &mut R) - where - R: Renderer + ConfigHolder, - C: CanvasAccess, - { - canvas.set_clipping(self.dest.clone()); - match self.file() { - Some(file) => file.render( - canvas, - renderer, - &RenderContext::RelativePosition(self.render_start_point()), - ), - _ => (), - }; - self.caret.render( - canvas, - &RenderContext::RelativePosition(self.render_start_point()), - ); - self.vertical_scroll_bar.render( - canvas, - &RenderContext::RelativePosition(self.dest.top_left()), - ); - self.horizontal_scroll_bar.render( - canvas, - &RenderContext::RelativePosition(self.dest.top_left()), - ); - } - - pub fn prepare_ui(&mut self, renderer: &mut T) - where - T: CharacterSizeManager, - { - 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(); - 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.clone() - } -} - -impl ConfigHolder for FileEditor { - fn config(&self) -> &ConfigAccess { - &self.config - } -} - -#[cfg(test)] -mod tests { - use crate::ui::*; - use rider_config::Config; - 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 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::tests::support; - use crate::ui::*; - use sdl2::rect::{Point, Rect}; - - 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_by(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/rider-editor/src/ui/file_editor/mod.rs b/rider-editor/src/ui/file_editor/mod.rs index 1c3e406..5e0d0b6 100644 --- a/rider-editor/src/ui/file_editor/mod.rs +++ b/rider-editor/src/ui/file_editor/mod.rs @@ -1,8 +1,27 @@ -pub use crate::ui::file_editor::file_editor::*; -use crate::ui::*; +use crate::app::UpdateResult as UR; +use crate::app::*; +use crate::renderer::renderer::Renderer; +use crate::ui::caret::caret::Caret; +use crate::ui::caret::caret_position::CaretPosition; +use crate::ui::caret::MoveDirection; +use crate::ui::file::editor_file::EditorFile; +use crate::ui::file::TextCollection; +use crate::ui::file::TextWidget; +use crate::ui::move_render_point; +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::text_character::CharacterSizeManager; +use crate::ui::CanvasAccess; +use crate::ui::ClickHandler; +use crate::ui::RenderBox; +use crate::ui::RenderContext; +use crate::ui::Update; +use crate::ui::UpdateContext; use sdl2::rect::Point; - -pub mod file_editor; +use sdl2::rect::Rect; +use std::mem; +use std::sync::*; pub trait FileAccess { fn has_file(&self) -> bool; @@ -33,3 +52,430 @@ pub trait ScrollableView { fn scroll(&self) -> Point; } + +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 R) + where + R: ConfigHolder + CharacterSizeManager + Renderer, + { + file_content_manager::delete_front(self, renderer); + } + + pub fn delete_back(&mut self, renderer: &mut R) + where + R: ConfigHolder + CharacterSizeManager + Renderer, + { + file_content_manager::delete_back(self, renderer); + } + + pub fn insert_text(&mut self, text: String, renderer: &mut R) + where + R: ConfigHolder + CharacterSizeManager + Renderer, + { + file_content_manager::insert_text(self, text, renderer); + } + + pub fn insert_new_line(&mut self, renderer: &mut R) + where + R: ConfigHolder + CharacterSizeManager + 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_by(&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(); + } + self.vertical_scroll_bar.set_location(0); + self.horizontal_scroll_bar.set_location(0); + 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 => caret_manager::move_caret_left(self), + 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 FileEditor { + pub fn render(&self, canvas: &mut C, renderer: &mut R) + where + R: Renderer + ConfigHolder, + C: CanvasAccess, + { + canvas.set_clipping(self.dest.clone()); + match self.file() { + Some(file) => file.render( + canvas, + renderer, + &RenderContext::RelativePosition(self.render_start_point()), + ), + _ => (), + }; + self.caret.render( + canvas, + &RenderContext::RelativePosition(self.render_start_point()), + ); + self.vertical_scroll_bar.render( + canvas, + &RenderContext::RelativePosition(self.dest.top_left()), + ); + self.horizontal_scroll_bar.render( + canvas, + &RenderContext::RelativePosition(self.dest.top_left()), + ); + } + + pub fn prepare_ui(&mut self, renderer: &mut T) + where + T: CharacterSizeManager, + { + 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(), + ) + }; + let editor_left_margin = match context { + UpdateContext::ParentPosition(p) => p.x() as u32, + _ => editor_left_margin as u32, + }; + self.dest.set_x(editor_left_margin.clone() as i32); + 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(); + 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.clone() + } +} + +impl ConfigHolder for FileEditor { + fn config(&self) -> &ConfigAccess { + &self.config + } +} + +#[cfg(test)] +mod tests { + use crate::ui::*; + use rider_config::Config; + 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 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::tests::support; + use crate::ui::*; + use sdl2::rect::{Point, Rect}; + + 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_by(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/rider-editor/src/ui/filesystem/directory.rs b/rider-editor/src/ui/filesystem/directory.rs index e124b04..4ff9f6d 100644 --- a/rider-editor/src/ui/filesystem/directory.rs +++ b/rider-editor/src/ui/filesystem/directory.rs @@ -64,11 +64,23 @@ impl DirectoryView { } } + // pub fn dest(&self) -> Rect { + // Rect::new( + // self.pos.x(), + // self.pos.y(), + // self.icon_width, + // self.icon_height, + // ) + // } + pub fn source(&self) -> &Rect { &self.source } - pub fn open_directory(&mut self, dir_path: String, renderer: &mut CanvasRenderer) -> bool { + pub fn open_directory(&mut self, dir_path: String, renderer: &mut R) -> bool + where + R: Renderer + CharacterSizeManager, + { match dir_path { _ if dir_path == self.path => { if !self.opened { @@ -127,7 +139,10 @@ impl DirectoryView { } } - fn read_directory(&mut self, renderer: &mut CanvasRenderer) { + fn read_directory(&mut self, renderer: &mut R) + where + R: Renderer + CharacterSizeManager, + { let entries: fs::ReadDir = match fs::read_dir(self.path.clone()) { Ok(d) => d, _ => return, @@ -167,9 +182,10 @@ impl DirectoryView { self.directories.sort_by(|a, b| a.name().cmp(&b.name())); } - fn render_icon(&self, canvas: &mut T, renderer: &mut CanvasRenderer, dest: &mut Rect) + fn render_icon(&self, canvas: &mut C, renderer: &mut R, dest: &mut Rect) where - T: CanvasAccess, + C: CanvasAccess, + R: Renderer, { let dir_texture_path = { let c = self.config.read().unwrap(); @@ -178,28 +194,25 @@ impl DirectoryView { themes_dir.push(path); themes_dir.to_str().unwrap().to_owned() }; - let texture = renderer - .texture_manager() - .load(dir_texture_path.as_str()) - .unwrap_or_else(|_| panic!("Failed to load directory entry texture")); - - canvas - .render_image( - texture, - self.source.clone(), - Rect::new(dest.x(), dest.y(), self.icon_width, self.icon_height), - ) - .unwrap_or_else(|_| panic!("Failed to draw directory entry texture")); + if let Ok(texture) = renderer.load_image(dir_texture_path.clone()) { + canvas + .render_image( + texture, + self.source.clone(), + Rect::new(dest.x(), dest.y(), self.icon_width, self.icon_height), + ) + .unwrap_or_else(|_| panic!("Failed to draw directory entry texture")); + } } - fn render_name(&self, canvas: &mut T, renderer: &mut CanvasRenderer, dest: &mut Rect) + fn render_name(&self, canvas: &mut C, renderer: &mut R, dest: &mut Rect) where - T: CanvasAccess, + C: CanvasAccess, + R: Renderer + CharacterSizeManager, { let mut d = dest.clone(); d.set_x(dest.x() + NAME_MARGIN); let font_details = build_font_details(self); - let font = renderer.font_manager().load(&font_details).unwrap(); let name = self.name(); let config = self.config.read().unwrap(); let text_color = config.theme().code_highlighting().title.color(); @@ -211,23 +224,24 @@ impl DirectoryView { text: c.to_string(), font: font_details.clone(), }; - let text_texture = renderer - .texture_manager() - .load_text(&mut text_details, font.clone()) - .unwrap(); - d.set_width(size.width()); - d.set_height(size.height()); + let maybe_texture = renderer.load_text_tex(&mut text_details, font_details.clone()); - canvas - .render_image(text_texture, self.source.clone(), d.clone()) - .unwrap_or_else(|_| panic!("Failed to draw directory entry texture")); - d.set_x(d.x() + size.width() as i32); + if let Ok(texture) = maybe_texture { + d.set_width(size.width()); + d.set_height(size.height()); + + canvas + .render_image(texture, self.source.clone(), d.clone()) + .unwrap_or_else(|_| panic!("Failed to draw directory entry texture")); + d.set_x(d.x() + size.width() as i32); + } } } - fn render_children(&self, canvas: &mut T, renderer: &mut CanvasRenderer, dest: &mut Rect) + fn render_children(&self, canvas: &mut C, renderer: &mut R, dest: &mut Rect) where - T: CanvasAccess, + C: CanvasAccess, + R: Renderer + CharacterSizeManager, { if !self.expanded { return; @@ -249,7 +263,10 @@ impl DirectoryView { } } - fn calculate_size(&mut self, renderer: &mut CanvasRenderer) { + fn calculate_size(&mut self, renderer: &mut R) + where + R: CharacterSizeManager, + { let size = renderer.load_character_size('W'); self.height = size.height(); self.icon_height = size.height(); @@ -277,19 +294,11 @@ impl DirectoryView { self.icon_height, ) } -} -impl ConfigHolder for DirectoryView { - fn config(&self) -> &ConfigAccess { - &self.config - } -} - -#[cfg_attr(tarpaulin, skip)] -impl DirectoryView { - pub fn render(&self, canvas: &mut T, renderer: &mut CanvasRenderer, context: &RenderContext) + pub fn render(&self, canvas: &mut C, renderer: &mut R, context: &RenderContext) where - T: CanvasAccess, + R: Renderer + CharacterSizeManager, + C: CanvasAccess, { let dest = self.dest(); let move_point = match context { @@ -297,12 +306,15 @@ impl DirectoryView { _ => Point::new(0, 0), }; let mut dest = move_render_point(move_point, &dest); - self.render_icon::(canvas, renderer, &mut dest); - self.render_name::(canvas, renderer, &mut dest.clone()); - self.render_children::(canvas, renderer, &mut dest); + self.render_icon::(canvas, renderer, &mut dest); + self.render_name::(canvas, renderer, &mut dest.clone()); + self.render_children::(canvas, renderer, &mut dest); } - pub fn prepare_ui(&mut self, renderer: &mut CanvasRenderer) { + pub fn prepare_ui(&mut self, renderer: &mut R) + where + R: Renderer + CharacterSizeManager, + { if self.opened { for dir in self.directories.iter_mut() { dir.prepare_ui(renderer); @@ -313,10 +325,8 @@ impl DirectoryView { } self.calculate_size(renderer); } -} -impl Update for DirectoryView { - fn update(&mut self, ticks: i32, context: &UpdateContext) -> UpdateResult { + pub fn update(&mut self, ticks: i32, context: &UpdateContext) -> UpdateResult { if !path::Path::new(&self.path).exists() { return UpdateResult::RefreshFsTree; } @@ -325,30 +335,17 @@ impl Update for DirectoryView { dir.update(ticks, context); } for file in self.files.iter_mut() { - file.update(ticks, context); + file.update(); } } UpdateResult::NoOp } -} -impl RenderBox for DirectoryView { - fn render_start_point(&self) -> Point { + pub fn render_start_point(&self) -> Point { self.pos.clone() } - fn dest(&self) -> Rect { - Rect::new( - self.pos.x(), - self.pos.y(), - self.icon_width, - self.icon_height, - ) - } -} - -impl ClickHandler for DirectoryView { - fn on_left_click(&mut self, point: &Point, context: &UpdateContext) -> UpdateResult { + pub fn on_left_click(&mut self, point: &Point, context: &UpdateContext) -> UpdateResult { let dest = self.dest(); let move_point = match context { &UpdateContext::ParentPosition(p) => p.clone(), @@ -381,7 +378,7 @@ impl ClickHandler for DirectoryView { for file in self.files.iter_mut() { let context = UpdateContext::ParentPosition(p.clone()); if file.is_left_click_target(&point, &context) { - return file.on_left_click(&point, &context); + return file.on_left_click(); } p = p + Point::new(0, file.height() as i32 + CHILD_MARGIN); } @@ -389,7 +386,7 @@ impl ClickHandler for DirectoryView { UpdateResult::NoOp } - fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool { + pub fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool { let dest = self.dest(); let move_point = match context { UpdateContext::ParentPosition(p) => p.clone(), @@ -430,3 +427,385 @@ impl ClickHandler for DirectoryView { false } } + +impl ConfigHolder for DirectoryView { + fn config(&self) -> &ConfigAccess { + &self.config + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::support::CanvasMock; + use crate::tests::support::SimpleRendererMock; + use crate::tests::support::{build_config, build_path}; + + //########################################################## + // name_width + //########################################################## + + #[test] + fn assert_initial_name_width() { + let config = build_config(); + let widget = DirectoryView::new("/foo".to_owned(), config); + assert_eq!(widget.name_width(), 0); + } + + #[test] + fn assert_prepared_name_width() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = DirectoryView::new("/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.name_width(), 39); + } + + //########################################################## + // icon_width + //########################################################## + + #[test] + fn assert_initial_icon_width() { + let config = build_config(); + let widget = DirectoryView::new("/foo".to_owned(), config); + assert_eq!(widget.icon_width(), 16); + } + + #[test] + fn assert_prepared_icon_width() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = DirectoryView::new("/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.icon_width(), 14); + } + + //########################################################## + // height + //########################################################## + + #[test] + fn assert_initial_height() { + let config = build_config(); + let widget = DirectoryView::new("/foo".to_owned(), config); + assert_eq!(widget.height(), 16); + } + + #[test] + fn assert_prepared_height() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = DirectoryView::new("/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.height(), 14); + } + + //########################################################## + // name + //########################################################## + + #[test] + fn assert_initial_name() { + let config = build_config(); + let widget = DirectoryView::new("/foo".to_owned(), config); + assert_eq!(widget.name(), "foo".to_owned()); + } + + #[test] + fn assert_prepared_name() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = DirectoryView::new("/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.name(), "foo".to_owned()); + } + + //########################################################## + // path + //########################################################## + + #[test] + fn assert_initial_path() { + let config = build_config(); + let widget = DirectoryView::new("/foo".to_owned(), config); + assert_eq!(widget.path(), "/foo".to_owned()); + } + + #[test] + fn assert_prepared_path() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = DirectoryView::new("/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.path(), "/foo".to_owned()); + } + + //########################################################## + // source + //########################################################## + + #[test] + fn assert_initial_source() { + let config = build_config(); + let widget = DirectoryView::new("/foo".to_owned(), config); + assert_eq!(widget.source(), &Rect::new(0, 0, 64, 64)); + } + + #[test] + fn assert_prepared_source() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = DirectoryView::new("/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.source(), &Rect::new(0, 0, 64, 64)); + } + + //########################################################## + // dest + //########################################################## + + #[test] + fn assert_initial_dest() { + let config = build_config(); + let widget = DirectoryView::new("/foo".to_owned(), config); + assert_eq!(widget.dest(), Rect::new(0, 0, 36, 16)); + } + + #[test] + fn assert_prepared_dest() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = DirectoryView::new("/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.dest(), Rect::new(0, 0, 73, 14)); + } + + //########################################################## + // update + //########################################################## + + #[test] + fn assert_update_when_doesnt_exists() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = DirectoryView::new("/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!( + widget.update(0, &UpdateContext::Nothing), + UpdateResult::RefreshFsTree + ); + } + + #[test] + fn assert_update_when_does_exists() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = DirectoryView::new("/tmp".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!( + widget.update(0, &UpdateContext::Nothing), + UpdateResult::NoOp + ); + } + + #[test] + fn assert_update_expanded() { + build_path("/tmp/rider-editor/directory-view-test".to_owned()); + + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = + DirectoryView::new("/tmp/rider-editor/directory-view-test".to_owned(), config); + widget.prepare_ui(&mut renderer); + widget.open_directory( + "/tmp/rider-editor/directory-view-test".to_owned(), + &mut renderer, + ); + widget.prepare_ui(&mut renderer); + assert_eq!( + widget.update(0, &UpdateContext::Nothing), + UpdateResult::NoOp + ); + } + + //########################################################## + // render + //########################################################## + + #[test] + fn assert_render_no_expanded() { + build_path("/tmp/rider-editor/directory-view-test".to_owned()); + + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let mut widget = + DirectoryView::new("/tmp/rider-editor/directory-view-test".to_owned(), config); + widget.prepare_ui(&mut renderer); + widget.render(&mut canvas, &mut renderer, &RenderContext::Nothing); + } + + #[test] + fn assert_render_expanded() { + build_path("/tmp/rider-editor/directory-view-test".to_owned()); + + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let mut widget = + DirectoryView::new("/tmp/rider-editor/directory-view-test".to_owned(), config); + widget.prepare_ui(&mut renderer); + widget.open_directory( + "/tmp/rider-editor/directory-view-test".to_owned(), + &mut renderer, + ); + widget.prepare_ui(&mut renderer); + widget.render(&mut canvas, &mut renderer, &RenderContext::Nothing); + } + + //########################################################## + // is_left_click_target + //########################################################## + + #[test] + fn assert_is_left_click_target_when_target() { + build_path("/tmp/rider-editor/directory-view-test".to_owned()); + + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let mut widget = DirectoryView::new("/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + widget.render(&mut canvas, &mut renderer, &RenderContext::Nothing); + let p = Point::new(0, 0); + let context = UpdateContext::Nothing; + assert_eq!(widget.is_left_click_target(&p, &context), true); + } + + #[test] + fn assert_is_left_click_target_when_target_with_parent() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let mut widget = DirectoryView::new("/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + widget.render(&mut canvas, &mut renderer, &RenderContext::Nothing); + let p = Point::new(0, 0); + let context = UpdateContext::ParentPosition(Point::new(0, 0)); + assert_eq!(widget.is_left_click_target(&p, &context), true); + } + + #[test] + fn assert_is_left_click_target_expanded() { + build_path("/tmp/rider-editor/directory-view-test".to_owned()); + + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let mut widget = + DirectoryView::new("/tmp/rider-editor/directory-view-test".to_owned(), config); + widget.prepare_ui(&mut renderer); + widget.open_directory( + "/tmp/rider-editor/directory-view-test".to_owned(), + &mut renderer, + ); + widget.prepare_ui(&mut renderer); + widget.render(&mut canvas, &mut renderer, &RenderContext::Nothing); + let p = Point::new(0, 0); + let context = UpdateContext::ParentPosition(Point::new(0, 0)); + assert_eq!(widget.is_left_click_target(&p, &context), true); + } + + #[test] + fn refute_is_left_click_target_when_target() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let mut widget = DirectoryView::new("/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + widget.render(&mut canvas, &mut renderer, &RenderContext::Nothing); + let p = Point::new(9000, 0); + let context = UpdateContext::Nothing; + assert_eq!(widget.is_left_click_target(&p, &context), false); + } + + #[test] + fn refute_is_left_click_target_when_target_with_parent() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let mut widget = DirectoryView::new("/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + widget.render(&mut canvas, &mut renderer, &RenderContext::Nothing); + let p = Point::new(0, 9000); + let context = UpdateContext::ParentPosition(Point::new(0, 0)); + assert_eq!(widget.is_left_click_target(&p, &context), false); + } + + //########################################################## + // on_left_click + //########################################################## + + #[test] + fn assert_on_left_click_when_target() { + build_path("/tmp/rider-editor/directory-view-test".to_owned()); + + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let mut widget = + DirectoryView::new("/tmp/rider-editor/directory-view-test".to_owned(), config); + widget.prepare_ui(&mut renderer); + widget.render(&mut canvas, &mut renderer, &RenderContext::Nothing); + let p = Point::new(0, 0); + let context = UpdateContext::Nothing; + assert_eq!( + widget.on_left_click(&p, &context), + UpdateResult::OpenDirectory("/tmp/rider-editor/directory-view-test".to_owned()) + ); + } + + #[test] + fn assert_on_left_click_when_target_with_parent() { + build_path("/tmp/rider-editor/directory-view-test".to_owned()); + + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let mut widget = + DirectoryView::new("/tmp/rider-editor/directory-view-test".to_owned(), config); + widget.prepare_ui(&mut renderer); + widget.render(&mut canvas, &mut renderer, &RenderContext::Nothing); + let p = Point::new(0, 0); + let context = UpdateContext::ParentPosition(Point::new(0, 0)); + assert_eq!( + widget.on_left_click(&p, &context), + UpdateResult::OpenDirectory("/tmp/rider-editor/directory-view-test".to_owned()) + ); + } + + #[test] + fn assert_on_left_click_expanded() { + build_path("/tmp/rider-editor/directory-view-test".to_owned()); + + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let mut widget = + DirectoryView::new("/tmp/rider-editor/directory-view-test".to_owned(), config); + widget.prepare_ui(&mut renderer); + widget.open_directory( + "/tmp/rider-editor/directory-view-test".to_owned(), + &mut renderer, + ); + widget.prepare_ui(&mut renderer); + widget.render(&mut canvas, &mut renderer, &RenderContext::Nothing); + let p = Point::new(0, 0); + let context = UpdateContext::ParentPosition(Point::new(0, 0)); + assert_eq!( + widget.on_left_click(&p, &context), + UpdateResult::OpenDirectory("/tmp/rider-editor/directory-view-test".to_owned()) + ); + } +} diff --git a/rider-editor/src/ui/filesystem/file.rs b/rider-editor/src/ui/filesystem/file.rs index a6fd92b..5513736 100644 --- a/rider-editor/src/ui/filesystem/file.rs +++ b/rider-editor/src/ui/filesystem/file.rs @@ -6,6 +6,11 @@ use sdl2::rect::{Point, Rect}; use std::collections::HashMap; use std::path; +const ICON_DEST_WIDTH: u32 = 16; +const ICON_DEST_HEIGHT: u32 = 16; +const ICON_SRC_WIDTH: u32 = 64; +const ICON_SRC_HEIGHT: u32 = 64; + pub struct FileEntry { name_width: u32, icon_width: u32, @@ -26,8 +31,8 @@ impl FileEntry { name_width: 0, icon_width: 0, height: 0, - dest: Rect::new(0, 0, 16, 16), - source: Rect::new(0, 0, 64, 64), + dest: Rect::new(0, 0, ICON_DEST_WIDTH, ICON_DEST_HEIGHT), + source: Rect::new(0, 0, ICON_SRC_WIDTH, ICON_SRC_HEIGHT), config, char_sizes: HashMap::new(), } @@ -70,9 +75,10 @@ impl FileEntry { ) } - fn render_icon(&self, canvas: &mut T, renderer: &mut CanvasRenderer, dest: &mut Rect) + fn render_icon(&self, canvas: &mut C, renderer: &mut R, dest: &mut Rect) where - T: CanvasAccess, + C: CanvasAccess, + R: Renderer, { let dir_texture_path = { let c = self.config.read().unwrap(); @@ -81,27 +87,25 @@ impl FileEntry { themes_dir.push(path); themes_dir.to_str().unwrap().to_owned() }; - let texture = renderer - .texture_manager() - .load(dir_texture_path.as_str()) - .unwrap_or_else(|_| panic!("Failed to load directory entry texture")); - dest.set_width(16); - dest.set_height(16); - canvas - .render_image(texture, self.source.clone(), dest.clone()) - .unwrap_or_else(|_| panic!("Failed to draw directory entry texture")); + let maybe_tex = renderer.load_image(dir_texture_path.clone()); + if let Ok(texture) = maybe_tex { + dest.set_width(ICON_DEST_WIDTH); + dest.set_height(ICON_DEST_HEIGHT); + canvas + .render_image(texture, self.source.clone(), dest.clone()) + .unwrap_or_else(|_| panic!("Failed to draw directory entry texture")); + } } - fn render_name(&self, canvas: &mut T, renderer: &mut CanvasRenderer, dest: &mut Rect) + fn render_name(&self, canvas: &mut C, renderer: &mut R, dest: &mut Rect) where - T: CanvasAccess, + C: CanvasAccess, + R: Renderer, { let mut d = dest.clone(); d.set_x(dest.x() + NAME_MARGIN); let font_details = build_font_details(self); - let font = renderer.font_manager().load(&font_details).unwrap(); - let texture_manager = renderer.texture_manager(); let name = self.name(); for c in name.chars() { @@ -115,31 +119,24 @@ impl FileEntry { text: c.to_string(), font: font_details.clone(), }; - let text_texture = texture_manager - .load_text(&mut text_details, font.clone()) - .unwrap(); - d.set_width(size.width()); - d.set_height(size.height()); + let maybe_texture = renderer.load_text_tex(&mut text_details, font_details.clone()); - canvas - .render_image(text_texture, self.source.clone(), d.clone()) - .unwrap_or_else(|_| panic!("Failed to draw directory entry texture")); - d.set_x(d.x() + size.width() as i32) + if let Ok(texture) = maybe_texture { + d.set_width(size.width()); + d.set_height(size.height()); + + canvas + .render_image(texture, self.source.clone(), d.clone()) + .unwrap_or_else(|_| panic!("Failed to draw directory entry texture")); + d.set_x(d.x() + size.width() as i32) + } } } -} -impl ConfigHolder for FileEntry { - fn config(&self) -> &ConfigAccess { - &self.config - } -} - -#[cfg_attr(tarpaulin, skip)] -impl FileEntry { - pub fn render(&self, canvas: &mut T, renderer: &mut CanvasRenderer, context: &RenderContext) + pub fn render(&self, canvas: &mut C, renderer: &mut R, context: &RenderContext) where - T: CanvasAccess, + C: CanvasAccess, + R: Renderer, { let mut dest = match context { &RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.dest), @@ -149,48 +146,41 @@ impl FileEntry { self.render_name(canvas, renderer, &mut dest.clone()); } - pub fn prepare_ui(&mut self, renderer: &mut CanvasRenderer) { - let w_rect = get_text_character_rect('W', renderer).unwrap(); + pub fn prepare_ui(&mut self, renderer: &mut R) + where + R: Renderer + CharacterSizeManager, + { + let w_rect = renderer.load_character_size('W'); self.char_sizes.insert('W', w_rect.clone()); self.height = w_rect.height(); self.icon_width = w_rect.height(); self.name_width = 0; for c in self.name().chars() { - let size = { get_text_character_rect(c.clone(), renderer).unwrap() }; + let size = { renderer.load_character_size(c.clone()) }; self.char_sizes.insert(c, size); self.name_width += size.width(); } self.dest.set_width(w_rect.height()); self.dest.set_height(w_rect.height()); } -} -impl Update for FileEntry { - fn update(&mut self, _ticks: i32, _context: &UpdateContext) -> UpdateResult { + pub fn update(&mut self) -> UpdateResult { if !path::Path::new(&self.path).exists() { return UpdateResult::RefreshFsTree; } UpdateResult::NoOp } -} -impl RenderBox for FileEntry { - fn render_start_point(&self) -> Point { + pub fn render_start_point(&self) -> Point { self.dest.top_left() } - fn dest(&self) -> Rect { - self.dest.clone() - } -} - -impl ClickHandler for FileEntry { - fn on_left_click(&mut self, _point: &Point, _context: &UpdateContext) -> UpdateResult { + pub fn on_left_click(&mut self) -> UpdateResult { UpdateResult::OpenFile(self.path.clone()) } - fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool { + pub fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool { let dest = Rect::new( self.dest.x(), self.dest.y(), @@ -204,3 +194,270 @@ impl ClickHandler for FileEntry { rect.contains_point(point.clone()) } } + +impl ConfigHolder for FileEntry { + fn config(&self) -> &ConfigAccess { + &self.config + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::support::build_config; + use crate::tests::support::CanvasMock; + use crate::tests::support::SimpleRendererMock; + + //########################################################## + // name_width + //########################################################## + + #[test] + fn assert_initial_name_width() { + let config = build_config(); + let widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + assert_eq!(widget.name_width(), 0); + } + + #[test] + fn assert_prepared_name_width() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.name_width(), 91); + } + + //########################################################## + // icon_width + //########################################################## + + #[test] + fn assert_initial_icon_width() { + let config = build_config(); + let widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + assert_eq!(widget.icon_width(), 0); + } + + #[test] + fn assert_prepared_icon_width() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.icon_width(), 14); + } + + //########################################################## + // height + //########################################################## + + #[test] + fn assert_initial_height() { + let config = build_config(); + let widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + assert_eq!(widget.height(), 0); + } + + #[test] + fn assert_prepared_height() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.height(), 14); + } + + //########################################################## + // name + //########################################################## + + #[test] + fn assert_initial_name() { + let config = build_config(); + let widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + assert_eq!(widget.name(), "bar.txt".to_owned()); + } + + #[test] + fn assert_prepared_name() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.name(), "bar.txt".to_owned()); + } + + //########################################################## + // path + //########################################################## + + #[test] + fn assert_initial_path() { + let config = build_config(); + let widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + assert_eq!(widget.path(), "/foo".to_owned()); + } + + #[test] + fn assert_prepared_path() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.path(), "/foo".to_owned()); + } + + //########################################################## + // source + //########################################################## + + #[test] + fn assert_initial_source() { + let config = build_config(); + let widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + assert_eq!(widget.source(), &Rect::new(0, 0, 64, 64)); + } + + #[test] + fn assert_prepared_source() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.source(), &Rect::new(0, 0, 64, 64)); + } + + //########################################################## + // dest + //########################################################## + + #[test] + fn assert_initial_dest() { + let config = build_config(); + let widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + assert_eq!(widget.dest(), &Rect::new(0, 0, 16, 16)); + } + + #[test] + fn assert_prepared_dest() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.dest(), &Rect::new(0, 0, 14, 14)); + } + + //########################################################## + // full_dest + //########################################################## + + #[test] + fn assert_initial_full_dest() { + let config = build_config(); + let widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + assert_eq!(widget.full_dest(), Rect::new(0, 0, 20, 1)); + } + + #[test] + fn assert_prepared_full_dest() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.full_dest(), Rect::new(0, 0, 125, 14)); + } + + //########################################################## + // update + //########################################################## + + #[test] + fn assert_update_when_doesnt_exists() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.update(), UpdateResult::RefreshFsTree); + } + + #[test] + fn assert_update_when_does_exists() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = FileEntry::new("bar.txt".to_owned(), "/tmp".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.update(), UpdateResult::NoOp); + } + + //########################################################## + // render + //########################################################## + + #[test] + fn assert_render() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let mut widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + widget.render(&mut canvas, &mut renderer, &RenderContext::Nothing); + assert_eq!(widget.full_dest(), Rect::new(0, 0, 125, 14)); + } + + //########################################################## + // is_left_click_target + //########################################################## + + #[test] + fn assert_is_left_click_target_when_target() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let mut widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + widget.render(&mut canvas, &mut renderer, &RenderContext::Nothing); + let p = Point::new(0, 0); + let context = UpdateContext::Nothing; + assert_eq!(widget.is_left_click_target(&p, &context), true); + } + + #[test] + fn assert_is_left_click_target_when_target_with_parent() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let mut widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + widget.render(&mut canvas, &mut renderer, &RenderContext::Nothing); + let p = Point::new(0, 0); + let context = UpdateContext::ParentPosition(Point::new(0, 0)); + assert_eq!(widget.is_left_click_target(&p, &context), true); + } + + #[test] + fn refute_is_left_click_target_when_target() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let mut widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + widget.render(&mut canvas, &mut renderer, &RenderContext::Nothing); + let p = Point::new(9000, 0); + let context = UpdateContext::Nothing; + assert_eq!(widget.is_left_click_target(&p, &context), false); + } + + #[test] + fn refute_is_left_click_target_when_target_with_parent() { + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let mut widget = FileEntry::new("bar.txt".to_owned(), "/foo".to_owned(), config); + widget.prepare_ui(&mut renderer); + widget.render(&mut canvas, &mut renderer, &RenderContext::Nothing); + let p = Point::new(0, 9000); + let context = UpdateContext::ParentPosition(Point::new(0, 0)); + assert_eq!(widget.is_left_click_target(&p, &context), false); + } +} diff --git a/rider-editor/src/ui/modal/open_file.rs b/rider-editor/src/ui/modal/open_file.rs index 4756cb3..70e583c 100644 --- a/rider-editor/src/ui/modal/open_file.rs +++ b/rider-editor/src/ui/modal/open_file.rs @@ -1,7 +1,8 @@ -use crate::renderer::CanvasRenderer; +use crate::renderer::renderer::Renderer; use crate::ui::*; use crate::ui::{RenderContext as RC, UpdateContext as UC}; use rider_config::ConfigAccess; +use rider_config::ConfigHolder; use sdl2::pixels::Color; use sdl2::rect::{Point, Rect}; use std::sync::Arc; @@ -56,7 +57,10 @@ impl OpenFile { self.root_path.clone() } - pub fn open_directory(&mut self, dir_path: String, renderer: &mut CanvasRenderer) { + pub fn open_directory(&mut self, dir_path: String, renderer: &mut R) + where + R: Renderer + CharacterSizeManager, + { self.directory_view.open_directory(dir_path, renderer); { let dest = self.directory_view.dest(); @@ -73,10 +77,8 @@ impl OpenFile { pub fn full_rect(&self) -> &Rect { &self.full_dest } -} -impl ScrollableView for OpenFile { - fn scroll_by(&mut self, x: i32, y: i32) { + pub fn scroll_by(&mut self, x: i32, y: i32) { let read_config = self.config.read().unwrap(); let value_x = read_config.scroll().speed() * x; @@ -98,16 +100,14 @@ impl ScrollableView for OpenFile { } } - fn scroll(&self) -> Point { + pub fn scroll(&self) -> Point { Point::new( -self.horizontal_scroll_bar.scroll_value(), -self.vertical_scroll_bar.scroll_value(), ) } -} -impl Update for OpenFile { - fn update(&mut self, ticks: i32, context: &UC) -> UR { + pub fn update(&mut self, ticks: i32, context: &UC) -> UR { let (window_width, window_height, color, scroll_width, scroll_margin) = { let c = self.config.read().unwrap(); ( @@ -123,6 +123,7 @@ impl Update for OpenFile { .set_x((window_width / 2) as i32 - (self.dest.width() / 2) as i32); self.dest .set_y((window_height / 2) as i32 - (self.dest.height() / 2) as i32); + self.background_color = color; // Scroll bars @@ -143,21 +144,19 @@ impl Update for OpenFile { // End UR::NoOp } -} -#[cfg_attr(tarpaulin, skip)] -impl OpenFile { - pub fn render(&self, canvas: &mut T, renderer: &mut CanvasRenderer, context: &RC) + pub fn render(&self, canvas: &mut C, renderer: &mut R, context: &RC) where - T: CanvasAccess, + C: CanvasAccess, + R: Renderer + CharacterSizeManager + ConfigHolder, { let dest = match context { RC::RelativePosition(p) => move_render_point(p.clone(), &self.dest), - _ => self.dest, + _ => self.dest.clone(), }; // Background - // canvas.set_clip_rect(dest.clone()); + canvas.set_clipping(dest.clone()); canvas .render_rect(dest, self.background_color) .unwrap_or_else(|_| panic!("Failed to render open file modal background!")); @@ -183,23 +182,22 @@ impl OpenFile { ); } - pub fn prepare_ui(&mut self, renderer: &mut CanvasRenderer) { + pub fn prepare_ui(&mut self, renderer: &mut R) + where + R: Renderer + CharacterSizeManager, + { self.directory_view.prepare_ui(renderer); } -} -impl RenderBox for OpenFile { - fn render_start_point(&self) -> Point { + pub fn render_start_point(&self) -> Point { self.dest.top_left() } - fn dest(&self) -> Rect { + pub fn dest(&self) -> Rect { self.dest.clone() } -} -impl ClickHandler for OpenFile { - fn on_left_click(&mut self, point: &Point, context: &UC) -> UR { + pub fn on_left_click(&mut self, point: &Point, context: &UC) -> UR { let dest = match context { UC::ParentPosition(p) => move_render_point(*p, &self.dest), _ => self.dest, @@ -221,15 +219,198 @@ impl ClickHandler for OpenFile { res } - fn is_left_click_target(&self, point: &Point, context: &UC) -> bool { + pub fn is_left_click_target(&self, point: &Point, context: &UC) -> bool { let dest = match context { UC::ParentPosition(p) => move_render_point(p.clone(), &self.dest), _ => self.dest.clone(), }; - let context = UC::ParentPosition( - dest.top_left() + Point::new(CONTENT_MARGIN_LEFT, CONTENT_MARGIN_TOP) + self.scroll(), - ); - self.directory_view.is_left_click_target(point, &context); - true + let p = + dest.top_left() + Point::new(CONTENT_MARGIN_LEFT, CONTENT_MARGIN_TOP) + self.scroll(); + let context = UC::ParentPosition(p); + if self.directory_view.is_left_click_target(point, &context) { + true + } else { + Rect::new(p.x(), p.y(), dest.width(), dest.height()).contains_point(point.clone()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::support::SimpleRendererMock; + use crate::tests::support::{build_config, CanvasMock}; + use std::fs; + + //####################################################################### + // scroll + //####################################################################### + + #[test] + fn assert_scroll() { + let config = build_config(); + let mut widget = OpenFile::new("/tmp".to_owned(), 100, 100, config); + widget.scroll_by(12, 13); + assert_eq!(widget.scroll(), Point::new(0, -390)); + } + + //####################################################################### + // dest + //####################################################################### + + #[test] + fn assert_dest() { + let config = build_config(); + let widget = OpenFile::new("/tmp".to_owned(), 120, 130, config); + assert_eq!(widget.dest(), Rect::new(452, 365, 120, 130)); + } + + //####################################################################### + // full_rect + //####################################################################### + + #[test] + fn assert_full_rect() { + let config = build_config(); + let widget = OpenFile::new("/tmp".to_owned(), 120, 130, config); + assert_eq!(widget.full_rect(), &Rect::new(0, 0, 16, 16)); + } + + //####################################################################### + // open_directory + //####################################################################### + + #[test] + fn assert_open_directory() { + let path = "/tmp/rider/test-open-file/open-directory"; + fs::create_dir_all(path).unwrap(); + let config = build_config(); + let mut renderer = SimpleRendererMock::new(config); + let mut widget = OpenFile::new(path.to_owned(), 120, 130, renderer.config().clone()); + widget.open_directory(path.to_owned(), &mut renderer); + } + + //####################################################################### + // update + //####################################################################### + + #[test] + fn assert_update() { + let config = build_config(); + let mut widget = OpenFile::new("/tmp".to_owned(), 100, 100, config); + widget.update(0, &UpdateContext::Nothing); + } + + //####################################################################### + // root_path + //####################################################################### + + #[test] + fn assert_root_path() { + let config = build_config(); + let path = "/tmp/rider/test-open-file/open-directory"; + let mut widget = OpenFile::new(path.to_owned(), 100, 100, config); + widget.update(0, &UpdateContext::Nothing); + assert_eq!(widget.root_path(), path.to_owned()); + } + + //####################################################################### + // render_start_point + //####################################################################### + + #[test] + fn assert_render_start_point() { + let config = build_config(); + let path = "/tmp/rider/test-open-file/open-directory"; + let mut widget = OpenFile::new(path.to_owned(), 100, 100, config); + widget.update(0, &UpdateContext::Nothing); + assert_eq!(widget.render_start_point(), Point::new(462, 380)); + } + + //####################################################################### + // on_left_click + //####################################################################### + + #[test] + fn assert_on_left_click_with_nothing() { + let config = build_config(); + let path = "/tmp/rider/test-open-file/open-directory"; + let mut widget = OpenFile::new(path.to_owned(), 100, 100, config); + let p = Point::new(100, 100); + let context = UpdateContext::Nothing; + widget.on_left_click(&p, &context); + } + + #[test] + fn assert_on_left_click_with_parent_position() { + let config = build_config(); + let path = "/tmp/rider/test-open-file/open-directory"; + let mut widget = OpenFile::new(path.to_owned(), 100, 100, config); + let p = Point::new(100, 100); + let context = UpdateContext::ParentPosition(Point::new(10, 10)); + widget.on_left_click(&p, &context); + } + + //####################################################################### + // is_left_click_target + //####################################################################### + + #[test] + fn assert_is_left_click_target_with_nothing() { + let config = build_config(); + let path = "/tmp/rider/test-open-file/open-directory"; + let widget = OpenFile::new(path.to_owned(), 100, 100, config); + let p = Point::new(100, 100); + let context = UpdateContext::Nothing; + assert_eq!(widget.is_left_click_target(&p, &context), false); + } + + #[test] + fn assert_is_left_click_target_with_parent_position() { + let config = build_config(); + let path = "/tmp/rider/test-open-file/open-directory"; + let widget = OpenFile::new(path.to_owned(), 100, 100, config); + let p = Point::new(100, 100); + let context = UpdateContext::ParentPosition(Point::new(10, 10)); + assert_eq!(widget.is_left_click_target(&p, &context), false); + } + + #[test] + fn assert_is_left_click_target_with_parent_position_in_box() { + let config = build_config(); + let path = "/tmp/rider/test-open-file/open-directory"; + let widget = OpenFile::new(path.to_owned(), 100, 100, config); + let p = Point::new(500, 400); + let context = UpdateContext::ParentPosition(Point::new(10, 10)); + assert_eq!(widget.is_left_click_target(&p, &context), false); + } + + //####################################################################### + // render + //####################################################################### + + #[test] + fn assert_render() { + let config = build_config(); + let path = "/tmp/rider/test-open-file/open-directory"; + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let widget = OpenFile::new(path.to_owned(), 100, 100, config); + let p = Point::new(100, 100); + let context = RenderContext::RelativePosition(p); + widget.render(&mut canvas, &mut renderer, &context); + } + + //####################################################################### + // prepare_ui + //####################################################################### + + #[test] + fn assert_prepare_ui() { + let config = build_config(); + let path = "/tmp/rider/test-open-file/open-directory"; + let mut renderer = SimpleRendererMock::new(config.clone()); + let mut widget = OpenFile::new(path.to_owned(), 100, 100, config); + widget.prepare_ui(&mut renderer); } } diff --git a/rider-editor/src/ui/project_tree/mod.rs b/rider-editor/src/ui/project_tree/mod.rs index 8b13789..384e1cd 100644 --- a/rider-editor/src/ui/project_tree/mod.rs +++ b/rider-editor/src/ui/project_tree/mod.rs @@ -1 +1,383 @@ +use crate::app::application::UpdateResult; +use crate::renderer::renderer::Renderer; +use crate::ui::file_editor::ScrollableView; +use crate::ui::filesystem::directory::DirectoryView; +use crate::ui::horizontal_scroll_bar::HorizontalScrollBar; +use crate::ui::move_render_point; +use crate::ui::scroll_bar::Scrollable; +use crate::ui::text_character::CharacterSizeManager; +use crate::ui::vertical_scroll_bar::VerticalScrollBar; +use crate::ui::CanvasAccess; +use crate::ui::ClickHandler; +use crate::ui::RenderContext; +use crate::ui::UpdateContext; +use rider_config::config::Config; +use sdl2::pixels::Color; +use sdl2::rect::Point; +use sdl2::rect::Rect; +use std::sync::Arc; +use std::sync::RwLock; +const CONTENT_MARGIN_LEFT: i32 = 16; +const CONTENT_MARGIN_TOP: i32 = 24; +const DEFAULT_ICON_SIZE: u32 = 16; + +pub struct ProjectTreeSidebar { + dest: Rect, + full_dest: Rect, + config: Arc>, + root: String, + border_color: Color, + background_color: Color, + dir_view: DirectoryView, + vertical_scroll_bar: VerticalScrollBar, + horizontal_scroll_bar: HorizontalScrollBar, +} + +impl ProjectTreeSidebar { + pub fn new(root: String, config: Arc>) -> Self { + let (background_color, border_color, h): (Color, Color, u32) = { + let c = config.read().unwrap(); + ( + c.theme().background().into(), + c.theme().border_color().into(), + c.height(), + ) + }; + + Self { + dest: Rect::new(0, 0, 200, h), + full_dest: Rect::new(0, 0, DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE), + dir_view: DirectoryView::new(root.clone(), config.clone()), + vertical_scroll_bar: VerticalScrollBar::new(Arc::clone(&config)), + horizontal_scroll_bar: HorizontalScrollBar::new(Arc::clone(&config)), + config, + root, + background_color, + border_color, + } + } + + pub fn update(&mut self, ticks: i32) { + let config = self.config.read().unwrap(); + let height = config.height(); + // let left_margin = config.editor_left_margin(); + let top_margin = config.menu_height() as i32; + // self.dest.set_x(left_margin); + self.dest.set_y(top_margin); + self.dest.set_height(height - top_margin as u32); + self.dir_view.update(ticks, &UpdateContext::Nothing); + } + + pub fn prepare_ui(&mut self, renderer: &mut R) + where + R: Renderer + CharacterSizeManager, + { + let config = self.config.read().unwrap(); + let height = config.height(); + let left_margin = 0; + let top_margin = config.menu_height() as i32; + self.dest.set_x(left_margin); + self.dest.set_y(top_margin); + self.dest.set_height(height); + self.dir_view.prepare_ui(renderer); + self.dir_view.open_directory(self.root.clone(), renderer); + } + + pub fn render(&self, canvas: &mut C, renderer: &mut R) + where + R: Renderer + CharacterSizeManager, + C: CanvasAccess, + { + canvas.set_clipping(self.dest.clone()); + canvas + .render_rect(self.dest.clone(), self.background_color.clone()) + .unwrap(); + canvas + .render_border(self.dest.clone(), self.border_color.clone()) + .unwrap(); + + // dir view + let context = RenderContext::RelativePosition( + self.dest.top_left() + Point::new(CONTENT_MARGIN_LEFT, CONTENT_MARGIN_TOP), + ); + self.dir_view.render(canvas, renderer, &context); + } + + pub fn full_rect(&self) -> Rect { + self.dest.clone() + } + + pub fn root(&self) -> String { + self.root.clone() + } + + pub fn open_directory(&mut self, dir_path: String, renderer: &mut R) + where + R: Renderer + CharacterSizeManager, + { + self.dir_view.open_directory(dir_path, renderer); + { + let dest = self.dir_view.dest(); + let full_dest = Rect::new( + dest.x(), + dest.y(), + dest.width() + (2 * CONTENT_MARGIN_LEFT as u32), + dest.height() + (2 * CONTENT_MARGIN_TOP as u32), + ); + self.full_dest = full_dest; + } + } +} + +impl ClickHandler for ProjectTreeSidebar { + fn on_left_click(&mut self, point: &Point, context: &UpdateContext) -> UpdateResult { + let dest = match context { + UpdateContext::ParentPosition(p) => move_render_point(*p, &self.dest), + _ => self.dest, + }; + let context = UpdateContext::ParentPosition( + dest.top_left() + Point::new(CONTENT_MARGIN_LEFT, CONTENT_MARGIN_TOP) + self.scroll(), + ); + let res = self.dir_view.on_left_click(point, &context); + { + let dest = self.dir_view.dest(); + let full_dest = Rect::new( + dest.x(), + dest.y(), + dest.width() + (2 * CONTENT_MARGIN_LEFT as u32), + dest.height() + (2 * CONTENT_MARGIN_TOP as u32), + ); + self.full_dest = full_dest; + } + res + } + + fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool { + let dest = match context { + UpdateContext::ParentPosition(p) => move_render_point(p.clone(), &self.dest), + _ => self.dest.clone(), + }; + let p = + dest.top_left() + Point::new(CONTENT_MARGIN_LEFT, CONTENT_MARGIN_TOP) + self.scroll(); + let context = UpdateContext::ParentPosition(p); + if self.dir_view.is_left_click_target(point, &context) { + true + } else { + Rect::new(p.x(), p.y(), dest.width(), dest.height()).contains_point(point.clone()) + } + } +} + +impl ScrollableView for ProjectTreeSidebar { + fn scroll_by(&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(), + ) + } +} + +#[cfg(test)] +mod tests { + use crate::renderer::managers::FontDetails; + use crate::renderer::managers::TextDetails; + use crate::renderer::renderer::Renderer; + use crate::tests::support::build_config; + use crate::tests::support::CanvasMock; + use crate::ui::file_editor::ScrollableView; + use crate::ui::project_tree::ProjectTreeSidebar; + use crate::ui::text_character::CharacterSizeManager; + use crate::ui::ClickHandler; + use crate::ui::UpdateContext; + use rider_config::ConfigAccess; + use rider_config::ConfigHolder; + use sdl2::rect::Point; + use sdl2::rect::Rect; + use sdl2::render::Texture; + use sdl2::ttf::Font; + use std::rc::Rc; + + #[cfg_attr(tarpaulin, skip)] + struct RendererMock { + config: ConfigAccess, + } + + #[cfg_attr(tarpaulin, skip)] + impl RendererMock { + pub fn new(config: ConfigAccess) -> Self { + Self { config } + } + } + + #[cfg_attr(tarpaulin, skip)] + impl Renderer for RendererMock { + fn load_font(&mut self, _details: FontDetails) -> Rc { + unimplemented!() + } + + fn load_text_tex( + &mut self, + _details: &mut TextDetails, + _font_details: FontDetails, + ) -> Result, String> { + Err("Skip load text texture".to_owned()) + } + + fn load_image(&mut self, _path: String) -> Result, String> { + Err("Skip render".to_owned()) + } + } + + #[cfg_attr(tarpaulin, skip)] + impl ConfigHolder for RendererMock { + fn config(&self) -> &ConfigAccess { + &self.config + } + } + + #[cfg_attr(tarpaulin, skip)] + impl CharacterSizeManager for RendererMock { + fn load_character_size(&mut self, _c: char) -> Rect { + Rect::new(0, 0, 13, 14) + } + } + + #[test] + fn assert_full_rect() { + let config = build_config(); + let mut renderer = RendererMock::new(config.clone()); + let mut widget = ProjectTreeSidebar::new("/tmp".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.full_rect(), Rect::new(0, 60, 200, 860)); + } + + #[test] + fn assert_update() { + let config = build_config(); + let mut widget = ProjectTreeSidebar::new("/tmp".to_owned(), config); + widget.update(0); + assert_eq!(widget.full_rect(), Rect::new(0, 60, 200, 800)); + } + + #[test] + fn assert_prepare_ui() { + let config = build_config(); + let mut renderer = RendererMock::new(config.clone()); + let mut widget = ProjectTreeSidebar::new("/tmp".to_owned(), config); + widget.prepare_ui(&mut renderer); + assert_eq!(widget.full_rect(), Rect::new(0, 60, 200, 860)); + } + + #[test] + fn assert_render() { + let config = build_config(); + let mut renderer = RendererMock::new(config.clone()); + let mut canvas = CanvasMock::new(); + let widget = ProjectTreeSidebar::new("/tmp".to_owned(), config); + widget.render(&mut canvas, &mut renderer); + } + + //####################################################################### + // scroll + //####################################################################### + + #[test] + fn assert_scroll() { + let config = build_config(); + let widget = ProjectTreeSidebar::new("/tmp".to_owned(), config); + let res = widget.scroll(); + let expected = Point::new(0, 0); + assert_eq!(res, expected); + } + + #[test] + fn assert_scroll_by() { + let config = build_config(); + let mut widget = ProjectTreeSidebar::new("/tmp".to_owned(), config); + widget.scroll_by(10, 10); + let res = widget.scroll(); + let expected = Point::new(0, -300); + assert_eq!(res, expected); + } + + //####################################################################### + // on_left_click + //####################################################################### + + #[test] + fn assert_on_left_click_with_nothing() { + let config = build_config(); + let path = "/tmp/rider/test-open-file/open-directory"; + let mut widget = ProjectTreeSidebar::new(path.to_owned(), config); + let p = Point::new(100, 100); + let context = UpdateContext::Nothing; + widget.on_left_click(&p, &context); + } + + #[test] + fn assert_on_left_click_with_parent_position() { + let config = build_config(); + let path = "/tmp/rider/test-open-file/open-directory"; + let mut widget = ProjectTreeSidebar::new(path.to_owned(), config); + let p = Point::new(100, 100); + let context = UpdateContext::ParentPosition(Point::new(10, 10)); + widget.on_left_click(&p, &context); + } + + //####################################################################### + // is_left_click_target + //####################################################################### + + #[test] + fn assert_is_left_click_target_with_nothing() { + let config = build_config(); + let path = "/tmp/rider/test-open-file/open-directory"; + let widget = ProjectTreeSidebar::new(path.to_owned(), config); + let p = Point::new(400, 400); + let context = UpdateContext::Nothing; + assert_eq!(widget.is_left_click_target(&p, &context), false); + } + + #[test] + fn assert_is_left_click_target_with_parent_position() { + let config = build_config(); + let path = "/tmp/rider/test-open-file/open-directory"; + let widget = ProjectTreeSidebar::new(path.to_owned(), config); + let p = Point::new(800, 800); + let context = UpdateContext::ParentPosition(Point::new(10, 10)); + assert_eq!(widget.is_left_click_target(&p, &context), false); + } + + #[test] + fn assert_is_left_click_target_with_parent_position_in_box() { + let config = build_config(); + let path = "/tmp/rider/test-open-file/open-directory"; + let widget = ProjectTreeSidebar::new(path.to_owned(), config); + let p = Point::new(500, 400); + let context = UpdateContext::ParentPosition(Point::new(10, 10)); + assert_eq!(widget.is_left_click_target(&p, &context), false); + } +}