Gh 9/project tree sidebar (#17)

* Make render testable

* Format code

* Fix render caret after input

* Add new lines between tests

* Fix fmt

* Add project tree to view

* Add drag, clean up code

* Add some tests

* Add scroll, response to click on folder

* Format code

* Test file entry

Format code

Test file entry

* Test dir view

* Fix character position after delete last at line

* Fmt

* Fix tests
This commit is contained in:
Adrian Woźniak 2019-05-18 10:35:11 +02:00 committed by GitHub
parent 245550a74e
commit c714ca15e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 2145 additions and 771 deletions

View File

@ -9,7 +9,7 @@ Text editor in rust
```bash ```bash
curl https://sh.rustup.rs -sSf | sh 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 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 ## Road map

View File

@ -10,7 +10,7 @@ impl ScrollConfig {
Self { Self {
width: 4, width: 4,
margin_right: 5, margin_right: 5,
speed: 10, speed: 30,
} }
} }
@ -91,7 +91,7 @@ mod tests {
fn assert_speed() { fn assert_speed() {
let config = ScrollConfig::new(); let config = ScrollConfig::new();
let result = config.speed(); let result = config.speed();
let expected = 10; let expected = 30;
assert_eq!(result, expected); assert_eq!(result, expected);
} }

View File

@ -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::renderer::CanvasRenderer;
use crate::ui::*; use crate::ui::*;
use rider_config::*; use rider_config::*;
@ -9,6 +11,7 @@ use std::sync::*;
pub struct AppState { pub struct AppState {
menu_bar: MenuBar, menu_bar: MenuBar,
project_tree: ProjectTreeSidebar,
files: Vec<EditorFile>, files: Vec<EditorFile>,
config: Arc<RwLock<Config>>, config: Arc<RwLock<Config>>,
file_editor: FileEditor, file_editor: FileEditor,
@ -19,6 +22,10 @@ impl AppState {
pub fn new(config: Arc<RwLock<Config>>) -> Self { pub fn new(config: Arc<RwLock<Config>>) -> Self {
Self { Self {
menu_bar: MenuBar::new(Arc::clone(&config)), menu_bar: MenuBar::new(Arc::clone(&config)),
project_tree: ProjectTreeSidebar::new(
Application::current_working_directory(),
config.clone(),
),
files: vec![], files: vec![],
file_editor: FileEditor::new(Arc::clone(&config)), file_editor: FileEditor::new(Arc::clone(&config)),
open_file_modal: None, open_file_modal: None,
@ -40,18 +47,22 @@ impl AppState {
}; };
} }
#[cfg_attr(tarpaulin, skip)] pub fn open_directory<R>(&mut self, dir_path: String, renderer: &mut R)
pub fn open_directory(&mut self, dir_path: String, renderer: &mut CanvasRenderer) { where
R: Renderer + CharacterSizeManager,
{
match self.open_file_modal.as_mut() { match self.open_file_modal.as_mut() {
Some(modal) => modal.open_directory(dir_path, renderer), 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 { pub fn file_editor(&self) -> &FileEditor {
&self.file_editor &self.file_editor
} }
#[cfg_attr(tarpaulin, skip)]
pub fn file_editor_mut(&mut self) -> &mut FileEditor { pub fn file_editor_mut(&mut self) -> &mut FileEditor {
&mut self.file_editor &mut self.file_editor
} }
@ -75,23 +86,38 @@ impl AppState {
#[cfg_attr(tarpaulin, skip)] #[cfg_attr(tarpaulin, skip)]
impl AppState { impl AppState {
pub fn render(&self, canvas: &mut WC, renderer: &mut CanvasRenderer, _context: &RenderContext) { pub fn render<C, R>(&self, canvas: &mut C, renderer: &mut R, _context: &RenderContext)
where
C: CanvasAccess,
R: Renderer + ConfigHolder + CharacterSizeManager,
{
// file editor
self.file_editor.render(canvas, renderer); self.file_editor.render(canvas, renderer);
// menu bar
self.menu_bar.render(canvas, &RenderContext::Nothing); 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() { match self.open_file_modal.as_ref() {
Some(modal) => modal.render(canvas, renderer, &RenderContext::Nothing), Some(modal) => modal.render(canvas, renderer, &RenderContext::Nothing),
_ => (), _ => (),
}; };
} }
pub fn prepare_ui(&mut self, renderer: &mut CanvasRenderer) { pub fn prepare_ui<R>(&mut self, renderer: &mut R)
where
R: Renderer + CharacterSizeManager,
{
self.menu_bar.prepare_ui(); self.menu_bar.prepare_ui();
self.project_tree.prepare_ui(renderer);
self.file_editor.prepare_ui(renderer); self.file_editor.prepare_ui(renderer);
} }
}
impl Update for AppState { pub fn update(&mut self, ticks: i32, context: &UpdateContext) -> UpdateResult {
fn update(&mut self, ticks: i32, context: &UpdateContext) -> UpdateResult { // open file modal
let res = match self.open_file_modal.as_mut() { let res = match self.open_file_modal.as_mut() {
Some(modal) => modal.update(ticks, &UpdateContext::Nothing), Some(modal) => modal.update(ticks, &UpdateContext::Nothing),
_ => UpdateResult::NoOp, _ => UpdateResult::NoOp,
@ -100,8 +126,17 @@ impl Update for AppState {
return res; return res;
} }
// menu bar
self.menu_bar.update(ticks, context); 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 UpdateResult::NoOp
} }
} }
@ -109,6 +144,14 @@ impl Update for AppState {
impl AppState { impl AppState {
#[cfg_attr(tarpaulin, skip)] #[cfg_attr(tarpaulin, skip)]
pub fn on_left_click(&mut self, point: &Point, video_subsystem: &mut VS) -> UpdateResult { 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() { match self.open_file_modal.as_mut() {
Some(modal) => return modal.on_left_click(point, &UpdateContext::Nothing), Some(modal) => return modal.on_left_click(point, &UpdateContext::Nothing),
_ => (), _ => (),

View File

@ -16,6 +16,7 @@ use sdl2::surface::Surface;
use sdl2::video::Window; use sdl2::video::Window;
use sdl2::EventPump; use sdl2::EventPump;
use sdl2::{Sdl, TimerSubsystem, VideoSubsystem}; use sdl2::{Sdl, TimerSubsystem, VideoSubsystem};
use std::env;
use std::process::Command; use std::process::Command;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::thread::sleep; use std::thread::sleep;
@ -29,6 +30,8 @@ pub enum UpdateResult {
Stop, Stop,
RefreshPositions, RefreshPositions,
MouseLeftClicked(Point), MouseLeftClicked(Point),
MouseDragStart(Point),
MouseDragStop(Point),
MoveCaret(Rect, CaretPosition), MoveCaret(Rect, CaretPosition),
DeleteFront, DeleteFront,
DeleteBack, DeleteBack,
@ -44,6 +47,7 @@ pub enum UpdateResult {
OpenFile(String), OpenFile(String),
OpenDirectory(String), OpenDirectory(String),
OpenFileModal, OpenFileModal,
FileDropped(String),
} }
#[cfg_attr(tarpaulin, skip)] #[cfg_attr(tarpaulin, skip)]
@ -187,14 +191,16 @@ impl Application {
app_state.open_directory(dir_path.clone(), &mut renderer); app_state.open_directory(dir_path.clone(), &mut renderer);
} }
UpdateResult::OpenFileModal => { UpdateResult::OpenFileModal => {
use std::env; let pwd = Self::current_working_directory();
let pwd = env::current_dir().unwrap().to_str().unwrap().to_string();
let mut modal = let mut modal =
OpenFile::new(pwd.clone(), 400, 800, Arc::clone(&self.config)); OpenFile::new(pwd.clone(), 400, 800, Arc::clone(&self.config));
modal.prepare_ui(&mut renderer); modal.prepare_ui(&mut renderer);
modal.open_directory(pwd.clone(), &mut renderer); modal.open_directory(pwd.clone(), &mut renderer);
app_state.set_open_file_modal(Some(modal)); app_state.set_open_file_modal(Some(modal));
} }
UpdateResult::MouseDragStart(_point) => (),
UpdateResult::MouseDragStop(_point) => (),
UpdateResult::FileDropped(_path) => (),
} }
} }
self.tasks = new_tasks; self.tasks = new_tasks;
@ -241,80 +247,78 @@ impl Application {
Event::Quit { .. } => self.tasks.push(UpdateResult::Stop), Event::Quit { .. } => self.tasks.push(UpdateResult::Stop),
Event::MouseButtonUp { Event::MouseButtonUp {
mouse_btn, x, y, .. mouse_btn, x, y, ..
} => match mouse_btn { } if mouse_btn == MouseButton::Left => {
MouseButton::Left => self self.tasks
.tasks .push(UpdateResult::MouseDragStart(Point::new(x, y)));
.push(UpdateResult::MouseLeftClicked(Point::new(x, y))), 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);
}
}
_ => {}
};
} }
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, .. } => { Event::TextInput { text, .. } => {
self.tasks.push(UpdateResult::Input(text)); self.tasks.push(UpdateResult::Input(text));
} }
Event::MouseWheel { Event::MouseWheel {
direction, x, y, .. direction, x, y, ..
} => { } => match direction {
match direction { MouseWheelDirection::Normal => {
MouseWheelDirection::Normal => { self.tasks.push(UpdateResult::Scroll { x, y });
self.tasks.push(UpdateResult::Scroll { x, y }); }
} MouseWheelDirection::Flipped => {
MouseWheelDirection::Flipped => { self.tasks.push(UpdateResult::Scroll { x, y: -y });
self.tasks.push(UpdateResult::Scroll { x, y: -y }); }
} _ => {
_ => { // ignore
// ignore }
} },
};
}
Event::Window { Event::Window {
win_event: WindowEvent::Resized(w, h), win_event: WindowEvent::Resized(w, h),
.. ..
} => { } => self.tasks.push(UpdateResult::WindowResize {
self.tasks.push(UpdateResult::WindowResize { width: w,
width: w, height: h,
height: h, }),
});
}
_ => {} _ => {}
} }
} }
} }
pub fn current_working_directory() -> String {
env::current_dir().unwrap().to_str().unwrap().to_string()
}
} }
#[cfg_attr(tarpaulin, skip)] #[cfg_attr(tarpaulin, skip)]

View File

@ -26,16 +26,18 @@ pub fn move_caret_left(file_editor: &mut FileEditor) {
if file_editor.caret().text_position() == 0 { if file_editor.caret().text_position() == 0 {
return; return;
} }
let c: TextCharacter = match file.get_character_at(file_editor.caret().text_position() - 1) { let text_character: TextCharacter =
Some(text_character) => text_character, match file.get_character_at(file_editor.caret().text_position() - 1) {
None => return, // EOF Some(text_character) => text_character,
}; None => return, // EOF
};
let pos = file_editor.caret().position(); 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); let p = pos.moved(-1, 0, 0);
file_editor file_editor.caret_mut().move_caret(
.caret_mut() p,
.move_caret(p, Point::new(d.x(), d.y())); Point::new(character_destination.x(), character_destination.y()),
);
} }
#[cfg(test)] #[cfg(test)]
@ -77,6 +79,11 @@ mod test_move_right {
) -> Result<Rc<Texture>, String> { ) -> Result<Rc<Texture>, String> {
Err("skip render character".to_owned()) Err("skip render character".to_owned())
} }
#[cfg_attr(tarpaulin, skip)]
fn load_image(&mut self, _path: String) -> Result<Rc<Texture>, String> {
unimplemented!()
}
} }
impl ConfigHolder for RendererMock { impl ConfigHolder for RendererMock {
@ -163,6 +170,11 @@ mod test_move_left {
unimplemented!() unimplemented!()
} }
#[cfg_attr(tarpaulin, skip)]
fn load_image(&mut self, _path: String) -> Result<Rc<Texture>, String> {
unimplemented!()
}
fn load_text_tex( fn load_text_tex(
&mut self, &mut self,
_details: &mut TextDetails, _details: &mut TextDetails,

View File

@ -15,12 +15,10 @@ pub fn delete_front<R>(file_editor: &mut FileEditor, renderer: &mut R)
where where
R: ConfigHolder + CharacterSizeManager + Renderer, R: ConfigHolder + CharacterSizeManager + Renderer,
{ {
let mut buffer: String = if let Some(file) = file_editor.file() { let mut buffer: String = match file_editor.file() {
file Some(file) => file.buffer(),
} else { _ => return,
return; };
}
.buffer();
let position: CaretPosition = file_editor.caret().position().clone(); let position: CaretPosition = file_editor.caret().position().clone();
if position.text_position() == 0 { if position.text_position() == 0 {
return; return;
@ -28,20 +26,16 @@ where
let c: char = buffer.chars().collect::<Vec<char>>()[position.text_position() - 1]; let c: char = buffer.chars().collect::<Vec<char>>()[position.text_position() - 1];
buffer.remove(position.text_position() - 1); buffer.remove(position.text_position() - 1);
let position = match c { let position = match c {
'\n' if position.text_position() > 0 && position.line_number() > 0 => { '\n' if !position.is_first() => position.moved(-1, -1, 0),
position.moved(-1, -1, 0)
}
'\n' => position.clone(), '\n' => position.clone(),
_ if position.text_position() > 0 => position.moved(-1, 0, 0), _ if position.text_position() > 0 => position.moved(-1, 0, 0),
_ => position.moved(0, 0, 0), _ => position.moved(0, 0, 0),
}; };
let move_to = file_editor let move_to = file_editor
.file() .file()
.and_then(|f| f.get_character_at(file_editor.caret().text_position())) .and_then(|f| f.get_character_at(position.text_position()))
.and_then(|character| { .map(|character| character.dest())
let dest: Rect = character.dest(); .map(|dest| (position, Point::new(dest.x(), dest.y())));
Some((position, Point::new(dest.x(), dest.y())))
});
match move_to { match move_to {
Some((position, point)) => file_editor.caret_mut().move_caret(position, point), Some((position, point)) => file_editor.caret_mut().move_caret(position, point),
None => file_editor.caret_mut().reset_caret(), None => file_editor.caret_mut().reset_caret(),
@ -60,10 +54,9 @@ pub fn delete_back<R>(file_editor: &mut FileEditor, renderer: &mut R)
where where
R: ConfigHolder + CharacterSizeManager + Renderer, R: ConfigHolder + CharacterSizeManager + Renderer,
{ {
let file: &EditorFile = if let Some(file) = file_editor.file() { let file: &EditorFile = match file_editor.file() {
file Some(file) => file,
} else { None => return,
return;
}; };
let mut buffer: String = file.buffer(); let mut buffer: String = file.buffer();
let position: usize = file_editor.caret().text_position(); let position: usize = file_editor.caret().text_position();
@ -178,6 +171,11 @@ mod tests {
) -> Result<Rc<Texture>, String> { ) -> Result<Rc<Texture>, String> {
Err("skip render character".to_owned()) Err("skip render character".to_owned())
} }
#[cfg_attr(tarpaulin, skip)]
fn load_image(&mut self, _path: String) -> Result<Rc<Texture>, String> {
unimplemented!()
}
} }
impl ConfigHolder for RendererMock { impl ConfigHolder for RendererMock {

View File

@ -7,3 +7,7 @@ pub use crate::app::app_state::*;
pub use crate::app::application::*; pub use crate::app::application::*;
pub use crate::app::caret_manager::*; pub use crate::app::caret_manager::*;
pub use crate::app::file_content_manager::*; pub use crate::app::file_content_manager::*;
pub trait Resize {
fn resize_element(&mut self);
}

View File

@ -18,6 +18,8 @@ pub trait Renderer {
details: &mut TextDetails, details: &mut TextDetails,
font_details: FontDetails, font_details: FontDetails,
) -> Result<Rc<Texture>, String>; ) -> Result<Rc<Texture>, String>;
fn load_image(&mut self, path: String) -> Result<Rc<Texture>, String>;
} }
#[cfg_attr(tarpaulin, skip)] #[cfg_attr(tarpaulin, skip)]
@ -117,4 +119,8 @@ impl<'l> Renderer for CanvasRenderer<'l> {
let tex_manager = self.texture_manager(); let tex_manager = self.texture_manager();
tex_manager.load_text(details, font) tex_manager.load_text(details, font)
} }
fn load_image(&mut self, path: String) -> Result<Rc<Texture>, String> {
self.texture_manager.load(path.as_str())
}
} }

View File

@ -1,11 +1,159 @@
#[cfg(test)] #[cfg(test)]
pub mod support { 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::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::*; 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<RwLock<Config>> { pub fn build_config() -> Arc<RwLock<Config>> {
let mut config = Config::new(); let mut config = Config::new();
config.set_theme(config.editor_config().current_theme().clone()); config.set_theme(config.editor_config().current_theme().clone());
Arc::new(RwLock::new(config)) 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<RendererRect>,
pub borders: Vec<RendererRect>,
pub lines: Vec<RendererRect>,
pub clippings: Vec<Rect>,
}
#[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<Texture>,
_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<Font> {
unimplemented!()
}
fn load_text_tex(
&mut self,
_details: &mut TextDetails,
_font_details: FontDetails,
) -> Result<Rc<Texture>, String> {
Err("skip text texture".to_owned())
}
fn load_image(&mut self, _path: String) -> Result<Rc<Texture>, 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<RwLock<Config>> {
&self.config
}
}
} }

View File

@ -99,7 +99,7 @@ impl Caret {
impl Caret { impl Caret {
pub fn update(&mut self) -> UR { pub fn update(&mut self) -> UR {
self.blink_delay += 1; self.blink_delay += 1;
if self.blink_delay >= 30 { if self.blink_delay >= 15 {
self.blink_delay = 0; self.blink_delay = 0;
self.toggle_state(); self.toggle_state();
} }

View File

@ -14,32 +14,39 @@ impl CaretPosition {
} }
} }
#[inline]
pub fn text_position(&self) -> usize { pub fn text_position(&self) -> usize {
self.text_position self.text_position
} }
#[inline]
pub fn line_number(&self) -> usize { pub fn line_number(&self) -> usize {
self.line_number self.line_number
} }
#[inline]
pub fn line_position(&self) -> usize { pub fn line_position(&self) -> usize {
self.line_position self.line_position
} }
#[inline]
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.text_position = 0; self.text_position = 0;
self.line_number = 0; self.line_number = 0;
self.line_position = 0; self.line_position = 0;
} }
#[inline]
pub fn set_text_position(&mut self, n: usize) { pub fn set_text_position(&mut self, n: usize) {
self.text_position = n; self.text_position = n;
} }
#[inline]
pub fn set_line_number(&mut self, n: usize) { pub fn set_line_number(&mut self, n: usize) {
self.line_number = n; self.line_number = n;
} }
#[inline]
pub fn set_line_position(&mut self, n: usize) { pub fn set_line_position(&mut self, n: usize) {
self.line_position = n; self.line_position = n;
} }
@ -51,6 +58,11 @@ impl CaretPosition {
line_position: (self.line_position as i32 + line_position) as usize, 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)] #[cfg(test)]

View File

@ -200,6 +200,7 @@ impl ClickHandler for EditorFileToken {
mod tests { mod tests {
use super::*; use super::*;
use crate::tests::support::build_config; use crate::tests::support::build_config;
use crate::tests::support::CanvasMock;
use rider_lexers::Token; use rider_lexers::Token;
use sdl2::pixels::PixelFormatEnum; use sdl2::pixels::PixelFormatEnum;
use sdl2::render::Texture; use sdl2::render::Texture;
@ -207,9 +208,6 @@ mod tests {
use sdl2::surface::Surface; use sdl2::surface::Surface;
use sdl2::surface::SurfaceContext; use sdl2::surface::SurfaceContext;
use sdl2::ttf::Font; use sdl2::ttf::Font;
use std::fmt::Debug;
use std::fmt::Error;
use std::fmt::Formatter;
use std::rc::Rc; use std::rc::Rc;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
@ -217,83 +215,6 @@ mod tests {
// models // models
//################################################## //##################################################
#[derive(Debug, PartialEq)]
struct RendererRect {
pub rect: Rect,
pub color: Color,
}
#[cfg_attr(tarpaulin, skip)]
struct CanvasMock {
pub rects: Vec<RendererRect>,
pub borders: Vec<RendererRect>,
pub lines: Vec<RendererRect>,
pub clippings: Vec<Rect>,
}
#[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<Texture>,
_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)] #[cfg_attr(tarpaulin, skip)]
struct RendererMock<'l> { struct RendererMock<'l> {
pub config: Arc<RwLock<Config>>, pub config: Arc<RwLock<Config>>,
@ -317,10 +238,16 @@ mod tests {
#[cfg_attr(tarpaulin, skip)] #[cfg_attr(tarpaulin, skip)]
impl<'l> Renderer for RendererMock<'l> { impl<'l> Renderer for RendererMock<'l> {
#[cfg_attr(tarpaulin, skip)]
fn load_font(&mut self, _details: FontDetails) -> Rc<Font> { fn load_font(&mut self, _details: FontDetails) -> Rc<Font> {
unimplemented!("load_font") unimplemented!("load_font")
} }
#[cfg_attr(tarpaulin, skip)]
fn load_image(&mut self, _path: String) -> Result<Rc<Texture>, String> {
unimplemented!()
}
fn load_text_tex( fn load_text_tex(
&mut self, &mut self,
_details: &mut TextDetails, _details: &mut TextDetails,
@ -367,6 +294,7 @@ mod tests {
let mut token = EditorFileToken::new(&token_type, false, config.clone()); let mut token = EditorFileToken::new(&token_type, false, config.clone());
token.prepare_ui(&mut renderer); token.prepare_ui(&mut renderer);
} }
#[test] #[test]
fn assert_keyword_to_color() { fn assert_keyword_to_color() {
let config = build_config(); let config = build_config();
@ -378,6 +306,7 @@ mod tests {
let mut token = EditorFileToken::new(&token_type, false, config.clone()); let mut token = EditorFileToken::new(&token_type, false, config.clone());
token.prepare_ui(&mut renderer); token.prepare_ui(&mut renderer);
} }
#[test] #[test]
fn assert_string_to_color() { fn assert_string_to_color() {
let config = build_config(); let config = build_config();
@ -389,6 +318,7 @@ mod tests {
let mut token = EditorFileToken::new(&token_type, false, config.clone()); let mut token = EditorFileToken::new(&token_type, false, config.clone());
token.prepare_ui(&mut renderer); token.prepare_ui(&mut renderer);
} }
#[test] #[test]
fn assert_identifier_to_color() { fn assert_identifier_to_color() {
let config = build_config(); let config = build_config();
@ -400,6 +330,7 @@ mod tests {
let mut token = EditorFileToken::new(&token_type, false, config.clone()); let mut token = EditorFileToken::new(&token_type, false, config.clone());
token.prepare_ui(&mut renderer); token.prepare_ui(&mut renderer);
} }
#[test] #[test]
fn assert_literal_to_color() { fn assert_literal_to_color() {
let config = build_config(); let config = build_config();
@ -411,6 +342,7 @@ mod tests {
let mut token = EditorFileToken::new(&token_type, false, config.clone()); let mut token = EditorFileToken::new(&token_type, false, config.clone());
token.prepare_ui(&mut renderer); token.prepare_ui(&mut renderer);
} }
#[test] #[test]
fn assert_comment_to_color() { fn assert_comment_to_color() {
let config = build_config(); let config = build_config();
@ -422,6 +354,7 @@ mod tests {
let mut token = EditorFileToken::new(&token_type, false, config.clone()); let mut token = EditorFileToken::new(&token_type, false, config.clone());
token.prepare_ui(&mut renderer); token.prepare_ui(&mut renderer);
} }
#[test] #[test]
fn assert_operator_to_color() { fn assert_operator_to_color() {
let config = build_config(); let config = build_config();
@ -433,6 +366,7 @@ mod tests {
let mut token = EditorFileToken::new(&token_type, false, config.clone()); let mut token = EditorFileToken::new(&token_type, false, config.clone());
token.prepare_ui(&mut renderer); token.prepare_ui(&mut renderer);
} }
#[test] #[test]
fn assert_separator_to_color() { fn assert_separator_to_color() {
let config = build_config(); let config = build_config();

View File

@ -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<EditorFile>,
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<R>(&mut self, renderer: &mut R)
where
R: ConfigHolder + CharacterSizeManager + Renderer,
{
file_content_manager::delete_front(self, renderer);
}
pub fn delete_back<R>(&mut self, renderer: &mut R)
where
R: ConfigHolder + CharacterSizeManager + Renderer,
{
file_content_manager::delete_back(self, renderer);
}
pub fn insert_text<R>(&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<R>(&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<EditorFile> {
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<EditorFile> {
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<R, C>(&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<T>(&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<Config> = 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);
}
}

View File

@ -1,8 +1,27 @@
pub use crate::ui::file_editor::file_editor::*; use crate::app::UpdateResult as UR;
use crate::ui::*; 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; use sdl2::rect::Point;
use sdl2::rect::Rect;
pub mod file_editor; use std::mem;
use std::sync::*;
pub trait FileAccess { pub trait FileAccess {
fn has_file(&self) -> bool; fn has_file(&self) -> bool;
@ -33,3 +52,430 @@ pub trait ScrollableView {
fn scroll(&self) -> Point; fn scroll(&self) -> Point;
} }
pub struct FileEditor {
dest: Rect,
full_rect: Rect,
caret: Caret,
file: Option<EditorFile>,
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<R>(&mut self, renderer: &mut R)
where
R: ConfigHolder + CharacterSizeManager + Renderer,
{
file_content_manager::delete_front(self, renderer);
}
pub fn delete_back<R>(&mut self, renderer: &mut R)
where
R: ConfigHolder + CharacterSizeManager + Renderer,
{
file_content_manager::delete_back(self, renderer);
}
pub fn insert_text<R>(&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<R>(&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<EditorFile> {
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<EditorFile> {
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<R, C>(&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<T>(&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<Config> = 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);
}
}

View File

@ -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 { pub fn source(&self) -> &Rect {
&self.source &self.source
} }
pub fn open_directory(&mut self, dir_path: String, renderer: &mut CanvasRenderer) -> bool { pub fn open_directory<R>(&mut self, dir_path: String, renderer: &mut R) -> bool
where
R: Renderer + CharacterSizeManager,
{
match dir_path { match dir_path {
_ if dir_path == self.path => { _ if dir_path == self.path => {
if !self.opened { if !self.opened {
@ -127,7 +139,10 @@ impl DirectoryView {
} }
} }
fn read_directory(&mut self, renderer: &mut CanvasRenderer) { fn read_directory<R>(&mut self, renderer: &mut R)
where
R: Renderer + CharacterSizeManager,
{
let entries: fs::ReadDir = match fs::read_dir(self.path.clone()) { let entries: fs::ReadDir = match fs::read_dir(self.path.clone()) {
Ok(d) => d, Ok(d) => d,
_ => return, _ => return,
@ -167,9 +182,10 @@ impl DirectoryView {
self.directories.sort_by(|a, b| a.name().cmp(&b.name())); self.directories.sort_by(|a, b| a.name().cmp(&b.name()));
} }
fn render_icon<T>(&self, canvas: &mut T, renderer: &mut CanvasRenderer, dest: &mut Rect) fn render_icon<C, R>(&self, canvas: &mut C, renderer: &mut R, dest: &mut Rect)
where where
T: CanvasAccess, C: CanvasAccess,
R: Renderer,
{ {
let dir_texture_path = { let dir_texture_path = {
let c = self.config.read().unwrap(); let c = self.config.read().unwrap();
@ -178,28 +194,25 @@ impl DirectoryView {
themes_dir.push(path); themes_dir.push(path);
themes_dir.to_str().unwrap().to_owned() themes_dir.to_str().unwrap().to_owned()
}; };
let texture = renderer if let Ok(texture) = renderer.load_image(dir_texture_path.clone()) {
.texture_manager() canvas
.load(dir_texture_path.as_str()) .render_image(
.unwrap_or_else(|_| panic!("Failed to load directory entry texture")); texture,
self.source.clone(),
canvas Rect::new(dest.x(), dest.y(), self.icon_width, self.icon_height),
.render_image( )
texture, .unwrap_or_else(|_| panic!("Failed to draw directory entry 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<T>(&self, canvas: &mut T, renderer: &mut CanvasRenderer, dest: &mut Rect) fn render_name<C, R>(&self, canvas: &mut C, renderer: &mut R, dest: &mut Rect)
where where
T: CanvasAccess, C: CanvasAccess,
R: Renderer + CharacterSizeManager,
{ {
let mut d = dest.clone(); let mut d = dest.clone();
d.set_x(dest.x() + NAME_MARGIN); d.set_x(dest.x() + NAME_MARGIN);
let font_details = build_font_details(self); let font_details = build_font_details(self);
let font = renderer.font_manager().load(&font_details).unwrap();
let name = self.name(); let name = self.name();
let config = self.config.read().unwrap(); let config = self.config.read().unwrap();
let text_color = config.theme().code_highlighting().title.color(); let text_color = config.theme().code_highlighting().title.color();
@ -211,23 +224,24 @@ impl DirectoryView {
text: c.to_string(), text: c.to_string(),
font: font_details.clone(), font: font_details.clone(),
}; };
let text_texture = renderer let maybe_texture = renderer.load_text_tex(&mut text_details, font_details.clone());
.texture_manager()
.load_text(&mut text_details, font.clone())
.unwrap();
d.set_width(size.width());
d.set_height(size.height());
canvas if let Ok(texture) = maybe_texture {
.render_image(text_texture, self.source.clone(), d.clone()) d.set_width(size.width());
.unwrap_or_else(|_| panic!("Failed to draw directory entry texture")); d.set_height(size.height());
d.set_x(d.x() + size.width() as i32);
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<T>(&self, canvas: &mut T, renderer: &mut CanvasRenderer, dest: &mut Rect) fn render_children<C, R>(&self, canvas: &mut C, renderer: &mut R, dest: &mut Rect)
where where
T: CanvasAccess, C: CanvasAccess,
R: Renderer + CharacterSizeManager,
{ {
if !self.expanded { if !self.expanded {
return; return;
@ -249,7 +263,10 @@ impl DirectoryView {
} }
} }
fn calculate_size(&mut self, renderer: &mut CanvasRenderer) { fn calculate_size<R>(&mut self, renderer: &mut R)
where
R: CharacterSizeManager,
{
let size = renderer.load_character_size('W'); let size = renderer.load_character_size('W');
self.height = size.height(); self.height = size.height();
self.icon_height = size.height(); self.icon_height = size.height();
@ -277,19 +294,11 @@ impl DirectoryView {
self.icon_height, self.icon_height,
) )
} }
}
impl ConfigHolder for DirectoryView { pub fn render<R, C>(&self, canvas: &mut C, renderer: &mut R, context: &RenderContext)
fn config(&self) -> &ConfigAccess {
&self.config
}
}
#[cfg_attr(tarpaulin, skip)]
impl DirectoryView {
pub fn render<T>(&self, canvas: &mut T, renderer: &mut CanvasRenderer, context: &RenderContext)
where where
T: CanvasAccess, R: Renderer + CharacterSizeManager,
C: CanvasAccess,
{ {
let dest = self.dest(); let dest = self.dest();
let move_point = match context { let move_point = match context {
@ -297,12 +306,15 @@ impl DirectoryView {
_ => Point::new(0, 0), _ => Point::new(0, 0),
}; };
let mut dest = move_render_point(move_point, &dest); let mut dest = move_render_point(move_point, &dest);
self.render_icon::<T>(canvas, renderer, &mut dest); self.render_icon::<C, R>(canvas, renderer, &mut dest);
self.render_name::<T>(canvas, renderer, &mut dest.clone()); self.render_name::<C, R>(canvas, renderer, &mut dest.clone());
self.render_children::<T>(canvas, renderer, &mut dest); self.render_children::<C, R>(canvas, renderer, &mut dest);
} }
pub fn prepare_ui(&mut self, renderer: &mut CanvasRenderer) { pub fn prepare_ui<R>(&mut self, renderer: &mut R)
where
R: Renderer + CharacterSizeManager,
{
if self.opened { if self.opened {
for dir in self.directories.iter_mut() { for dir in self.directories.iter_mut() {
dir.prepare_ui(renderer); dir.prepare_ui(renderer);
@ -313,10 +325,8 @@ impl DirectoryView {
} }
self.calculate_size(renderer); self.calculate_size(renderer);
} }
}
impl Update for DirectoryView { pub fn update(&mut self, ticks: i32, context: &UpdateContext) -> UpdateResult {
fn update(&mut self, ticks: i32, context: &UpdateContext) -> UpdateResult {
if !path::Path::new(&self.path).exists() { if !path::Path::new(&self.path).exists() {
return UpdateResult::RefreshFsTree; return UpdateResult::RefreshFsTree;
} }
@ -325,30 +335,17 @@ impl Update for DirectoryView {
dir.update(ticks, context); dir.update(ticks, context);
} }
for file in self.files.iter_mut() { for file in self.files.iter_mut() {
file.update(ticks, context); file.update();
} }
} }
UpdateResult::NoOp UpdateResult::NoOp
} }
}
impl RenderBox for DirectoryView { pub fn render_start_point(&self) -> Point {
fn render_start_point(&self) -> Point {
self.pos.clone() self.pos.clone()
} }
fn dest(&self) -> Rect { pub fn on_left_click(&mut self, point: &Point, context: &UpdateContext) -> UpdateResult {
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 {
let dest = self.dest(); let dest = self.dest();
let move_point = match context { let move_point = match context {
&UpdateContext::ParentPosition(p) => p.clone(), &UpdateContext::ParentPosition(p) => p.clone(),
@ -381,7 +378,7 @@ impl ClickHandler for DirectoryView {
for file in self.files.iter_mut() { for file in self.files.iter_mut() {
let context = UpdateContext::ParentPosition(p.clone()); let context = UpdateContext::ParentPosition(p.clone());
if file.is_left_click_target(&point, &context) { 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); p = p + Point::new(0, file.height() as i32 + CHILD_MARGIN);
} }
@ -389,7 +386,7 @@ impl ClickHandler for DirectoryView {
UpdateResult::NoOp 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 dest = self.dest();
let move_point = match context { let move_point = match context {
UpdateContext::ParentPosition(p) => p.clone(), UpdateContext::ParentPosition(p) => p.clone(),
@ -430,3 +427,385 @@ impl ClickHandler for DirectoryView {
false 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())
);
}
}

View File

@ -6,6 +6,11 @@ use sdl2::rect::{Point, Rect};
use std::collections::HashMap; use std::collections::HashMap;
use std::path; 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 { pub struct FileEntry {
name_width: u32, name_width: u32,
icon_width: u32, icon_width: u32,
@ -26,8 +31,8 @@ impl FileEntry {
name_width: 0, name_width: 0,
icon_width: 0, icon_width: 0,
height: 0, height: 0,
dest: Rect::new(0, 0, 16, 16), dest: Rect::new(0, 0, ICON_DEST_WIDTH, ICON_DEST_HEIGHT),
source: Rect::new(0, 0, 64, 64), source: Rect::new(0, 0, ICON_SRC_WIDTH, ICON_SRC_HEIGHT),
config, config,
char_sizes: HashMap::new(), char_sizes: HashMap::new(),
} }
@ -70,9 +75,10 @@ impl FileEntry {
) )
} }
fn render_icon<T>(&self, canvas: &mut T, renderer: &mut CanvasRenderer, dest: &mut Rect) fn render_icon<C, R>(&self, canvas: &mut C, renderer: &mut R, dest: &mut Rect)
where where
T: CanvasAccess, C: CanvasAccess,
R: Renderer,
{ {
let dir_texture_path = { let dir_texture_path = {
let c = self.config.read().unwrap(); let c = self.config.read().unwrap();
@ -81,27 +87,25 @@ impl FileEntry {
themes_dir.push(path); themes_dir.push(path);
themes_dir.to_str().unwrap().to_owned() themes_dir.to_str().unwrap().to_owned()
}; };
let texture = renderer let maybe_tex = renderer.load_image(dir_texture_path.clone());
.texture_manager() if let Ok(texture) = maybe_tex {
.load(dir_texture_path.as_str()) dest.set_width(ICON_DEST_WIDTH);
.unwrap_or_else(|_| panic!("Failed to load directory entry texture")); dest.set_height(ICON_DEST_HEIGHT);
dest.set_width(16); canvas
dest.set_height(16); .render_image(texture, self.source.clone(), dest.clone())
canvas .unwrap_or_else(|_| panic!("Failed to draw directory entry texture"));
.render_image(texture, self.source.clone(), dest.clone()) }
.unwrap_or_else(|_| panic!("Failed to draw directory entry texture"));
} }
fn render_name<T>(&self, canvas: &mut T, renderer: &mut CanvasRenderer, dest: &mut Rect) fn render_name<C, R>(&self, canvas: &mut C, renderer: &mut R, dest: &mut Rect)
where where
T: CanvasAccess, C: CanvasAccess,
R: Renderer,
{ {
let mut d = dest.clone(); let mut d = dest.clone();
d.set_x(dest.x() + NAME_MARGIN); d.set_x(dest.x() + NAME_MARGIN);
let font_details = build_font_details(self); 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(); let name = self.name();
for c in name.chars() { for c in name.chars() {
@ -115,31 +119,24 @@ impl FileEntry {
text: c.to_string(), text: c.to_string(),
font: font_details.clone(), font: font_details.clone(),
}; };
let text_texture = texture_manager let maybe_texture = renderer.load_text_tex(&mut text_details, font_details.clone());
.load_text(&mut text_details, font.clone())
.unwrap();
d.set_width(size.width());
d.set_height(size.height());
canvas if let Ok(texture) = maybe_texture {
.render_image(text_texture, self.source.clone(), d.clone()) d.set_width(size.width());
.unwrap_or_else(|_| panic!("Failed to draw directory entry texture")); d.set_height(size.height());
d.set_x(d.x() + size.width() as i32)
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 { pub fn render<C, R>(&self, canvas: &mut C, renderer: &mut R, context: &RenderContext)
fn config(&self) -> &ConfigAccess {
&self.config
}
}
#[cfg_attr(tarpaulin, skip)]
impl FileEntry {
pub fn render<T>(&self, canvas: &mut T, renderer: &mut CanvasRenderer, context: &RenderContext)
where where
T: CanvasAccess, C: CanvasAccess,
R: Renderer,
{ {
let mut dest = match context { let mut dest = match context {
&RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.dest), &RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.dest),
@ -149,48 +146,41 @@ impl FileEntry {
self.render_name(canvas, renderer, &mut dest.clone()); self.render_name(canvas, renderer, &mut dest.clone());
} }
pub fn prepare_ui(&mut self, renderer: &mut CanvasRenderer) { pub fn prepare_ui<R>(&mut self, renderer: &mut R)
let w_rect = get_text_character_rect('W', renderer).unwrap(); where
R: Renderer + CharacterSizeManager,
{
let w_rect = renderer.load_character_size('W');
self.char_sizes.insert('W', w_rect.clone()); self.char_sizes.insert('W', w_rect.clone());
self.height = w_rect.height(); self.height = w_rect.height();
self.icon_width = w_rect.height(); self.icon_width = w_rect.height();
self.name_width = 0; self.name_width = 0;
for c in self.name().chars() { 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.char_sizes.insert(c, size);
self.name_width += size.width(); self.name_width += size.width();
} }
self.dest.set_width(w_rect.height()); self.dest.set_width(w_rect.height());
self.dest.set_height(w_rect.height()); self.dest.set_height(w_rect.height());
} }
}
impl Update for FileEntry { pub fn update(&mut self) -> UpdateResult {
fn update(&mut self, _ticks: i32, _context: &UpdateContext) -> UpdateResult {
if !path::Path::new(&self.path).exists() { if !path::Path::new(&self.path).exists() {
return UpdateResult::RefreshFsTree; return UpdateResult::RefreshFsTree;
} }
UpdateResult::NoOp UpdateResult::NoOp
} }
}
impl RenderBox for FileEntry { pub fn render_start_point(&self) -> Point {
fn render_start_point(&self) -> Point {
self.dest.top_left() self.dest.top_left()
} }
fn dest(&self) -> Rect { pub fn on_left_click(&mut self) -> UpdateResult {
self.dest.clone()
}
}
impl ClickHandler for FileEntry {
fn on_left_click(&mut self, _point: &Point, _context: &UpdateContext) -> UpdateResult {
UpdateResult::OpenFile(self.path.clone()) 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( let dest = Rect::new(
self.dest.x(), self.dest.x(),
self.dest.y(), self.dest.y(),
@ -204,3 +194,270 @@ impl ClickHandler for FileEntry {
rect.contains_point(point.clone()) 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);
}
}

View File

@ -1,7 +1,8 @@
use crate::renderer::CanvasRenderer; use crate::renderer::renderer::Renderer;
use crate::ui::*; use crate::ui::*;
use crate::ui::{RenderContext as RC, UpdateContext as UC}; use crate::ui::{RenderContext as RC, UpdateContext as UC};
use rider_config::ConfigAccess; use rider_config::ConfigAccess;
use rider_config::ConfigHolder;
use sdl2::pixels::Color; use sdl2::pixels::Color;
use sdl2::rect::{Point, Rect}; use sdl2::rect::{Point, Rect};
use std::sync::Arc; use std::sync::Arc;
@ -56,7 +57,10 @@ impl OpenFile {
self.root_path.clone() self.root_path.clone()
} }
pub fn open_directory(&mut self, dir_path: String, renderer: &mut CanvasRenderer) { pub fn open_directory<R>(&mut self, dir_path: String, renderer: &mut R)
where
R: Renderer + CharacterSizeManager,
{
self.directory_view.open_directory(dir_path, renderer); self.directory_view.open_directory(dir_path, renderer);
{ {
let dest = self.directory_view.dest(); let dest = self.directory_view.dest();
@ -73,10 +77,8 @@ impl OpenFile {
pub fn full_rect(&self) -> &Rect { pub fn full_rect(&self) -> &Rect {
&self.full_dest &self.full_dest
} }
}
impl ScrollableView for OpenFile { pub fn scroll_by(&mut self, x: i32, y: i32) {
fn scroll_by(&mut self, x: i32, y: i32) {
let read_config = self.config.read().unwrap(); let read_config = self.config.read().unwrap();
let value_x = read_config.scroll().speed() * x; 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( Point::new(
-self.horizontal_scroll_bar.scroll_value(), -self.horizontal_scroll_bar.scroll_value(),
-self.vertical_scroll_bar.scroll_value(), -self.vertical_scroll_bar.scroll_value(),
) )
} }
}
impl Update for OpenFile { pub fn update(&mut self, ticks: i32, context: &UC) -> UR {
fn update(&mut self, ticks: i32, context: &UC) -> UR {
let (window_width, window_height, color, scroll_width, scroll_margin) = { let (window_width, window_height, color, scroll_width, scroll_margin) = {
let c = self.config.read().unwrap(); 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); .set_x((window_width / 2) as i32 - (self.dest.width() / 2) as i32);
self.dest self.dest
.set_y((window_height / 2) as i32 - (self.dest.height() / 2) as i32); .set_y((window_height / 2) as i32 - (self.dest.height() / 2) as i32);
self.background_color = color; self.background_color = color;
// Scroll bars // Scroll bars
@ -143,21 +144,19 @@ impl Update for OpenFile {
// End // End
UR::NoOp UR::NoOp
} }
}
#[cfg_attr(tarpaulin, skip)] pub fn render<C, R>(&self, canvas: &mut C, renderer: &mut R, context: &RC)
impl OpenFile {
pub fn render<T>(&self, canvas: &mut T, renderer: &mut CanvasRenderer, context: &RC)
where where
T: CanvasAccess, C: CanvasAccess,
R: Renderer + CharacterSizeManager + ConfigHolder,
{ {
let dest = match context { let dest = match context {
RC::RelativePosition(p) => move_render_point(p.clone(), &self.dest), RC::RelativePosition(p) => move_render_point(p.clone(), &self.dest),
_ => self.dest, _ => self.dest.clone(),
}; };
// Background // Background
// canvas.set_clip_rect(dest.clone()); canvas.set_clipping(dest.clone());
canvas canvas
.render_rect(dest, self.background_color) .render_rect(dest, self.background_color)
.unwrap_or_else(|_| panic!("Failed to render open file modal background!")); .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<R>(&mut self, renderer: &mut R)
where
R: Renderer + CharacterSizeManager,
{
self.directory_view.prepare_ui(renderer); self.directory_view.prepare_ui(renderer);
} }
}
impl RenderBox for OpenFile { pub fn render_start_point(&self) -> Point {
fn render_start_point(&self) -> Point {
self.dest.top_left() self.dest.top_left()
} }
fn dest(&self) -> Rect { pub fn dest(&self) -> Rect {
self.dest.clone() self.dest.clone()
} }
}
impl ClickHandler for OpenFile { pub fn on_left_click(&mut self, point: &Point, context: &UC) -> UR {
fn on_left_click(&mut self, point: &Point, context: &UC) -> UR {
let dest = match context { let dest = match context {
UC::ParentPosition(p) => move_render_point(*p, &self.dest), UC::ParentPosition(p) => move_render_point(*p, &self.dest),
_ => self.dest, _ => self.dest,
@ -221,15 +219,198 @@ impl ClickHandler for OpenFile {
res 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 { let dest = match context {
UC::ParentPosition(p) => move_render_point(p.clone(), &self.dest), UC::ParentPosition(p) => move_render_point(p.clone(), &self.dest),
_ => self.dest.clone(), _ => self.dest.clone(),
}; };
let context = UC::ParentPosition( let p =
dest.top_left() + Point::new(CONTENT_MARGIN_LEFT, CONTENT_MARGIN_TOP) + self.scroll(), dest.top_left() + Point::new(CONTENT_MARGIN_LEFT, CONTENT_MARGIN_TOP) + self.scroll();
); let context = UC::ParentPosition(p);
self.directory_view.is_left_click_target(point, &context); if self.directory_view.is_left_click_target(point, &context) {
true 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);
} }
} }

View File

@ -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<RwLock<Config>>,
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<RwLock<Config>>) -> 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<R>(&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<C, R>(&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<R>(&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<Font> {
unimplemented!()
}
fn load_text_tex(
&mut self,
_details: &mut TextDetails,
_font_details: FontDetails,
) -> Result<Rc<Texture>, String> {
Err("Skip load text texture".to_owned())
}
fn load_image(&mut self, _path: String) -> Result<Rc<Texture>, 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);
}
}