Scroll and scrollbars (#14)

* Refactor, add tests for scroll related parts

* Add calculate text widget size and some boundaries logic

* Add appveyor config file

* Vertical bar

* Add horizontal scroll bar and fix click target

* Test scrolls

* Format code

* Fix scroll with different file content and window size

* Format code, updare readme

* Fix tests
This commit is contained in:
Adrian Woźniak 2019-01-08 20:16:58 +01:00 committed by GitHub
parent 4e13b4d2d7
commit 0380e3c9fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1216 additions and 335 deletions

View File

@ -1,6 +1,17 @@
[![codecov](https://codecov.io/gh/Eraden/rider/branch/master/graph/badge.svg)](https://codecov.io/gh/Eraden/rider)
[![CircleCI](https://circleci.com/gh/Eraden/rider.svg?style=svg&circle-token=546aae50b559665bd1f77a6452eff25e26a9d966)](https://circleci.com/gh/Eraden/rider)
# rider # rider
Text editor in rust Text editor in rust
## Build
```bash
curl https://sh.rustup.rs -sSf | sh
sudo apt-get install -q -y libsdl2-dev libsdl2-2.0-0 libsdl2-gfx-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-net-dev libsdl2-ttf-dev
rustup run nightly cargo build
```
## Road map ## Road map
### v1.0 ### v1.0
@ -15,10 +26,10 @@ Text editor in rust
* [ ] `Save file` with shortcut * [ ] `Save file` with shortcut
* [ ] `Save file as...` with shortcut * [ ] `Save file as...` with shortcut
* [x] Theme based menu UI * [x] Theme based menu UI
* [ ] Lock scroll when no available content * [x] Lock scroll when no available content
* [ ] Config edit menu * [ ] Config edit menu
* [ ] Project tree * [ ] Project tree
* [ ] Cover `rider` with tests at least 50% * [x] Cover `rider` with tests at least 50%
* [x] Handle resize window * [x] Handle resize window
* [ ] Selection * [ ] Selection

View File

@ -57,9 +57,11 @@ impl AppState {
} }
impl Render for AppState { impl Render for AppState {
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, _parent: Parent) { fn render(&self, canvas: &mut WC, renderer: &mut Renderer, _context: &RenderContext) {
self.file_editor.render(canvas, renderer, None); self.file_editor
self.menu_bar.render(canvas, renderer, None); .render(canvas, renderer, &RenderContext::Nothing);
self.menu_bar
.render(canvas, renderer, &RenderContext::Nothing);
} }
fn prepare_ui(&mut self, renderer: &mut Renderer) { fn prepare_ui(&mut self, renderer: &mut Renderer) {

View File

@ -147,7 +147,7 @@ impl Application {
app_state.file_editor_mut().move_caret(MoveDirection::Down); app_state.file_editor_mut().move_caret(MoveDirection::Down);
} }
UpdateResult::Scroll { x, y } => { UpdateResult::Scroll { x, y } => {
app_state.file_editor_mut().scroll_to(x, y); app_state.file_editor_mut().scroll_to(-x, -y);
} }
UpdateResult::WindowResize { width, height } => { UpdateResult::WindowResize { width, height } => {
let mut c = app_state.config().write().unwrap(); let mut c = app_state.config().write().unwrap();
@ -172,7 +172,7 @@ impl Application {
self.clear(); self.clear();
app_state.update(timer.ticks() as i32, &UpdateContext::Nothing); app_state.update(timer.ticks() as i32, &UpdateContext::Nothing);
app_state.render(&mut self.canvas, &mut renderer, None); app_state.render(&mut self.canvas, &mut renderer, &RenderContext::Nothing);
self.present(); self.present();
sleep(sleep_time); sleep(sleep_time);

View File

@ -1,5 +1,6 @@
use crate::config::creator; use crate::config::creator;
use crate::config::EditorConfig; use crate::config::EditorConfig;
use crate::config::ScrollConfig;
use crate::lexer::Language; use crate::lexer::Language;
use crate::themes::Theme; use crate::themes::Theme;
use dirs; use dirs;
@ -12,11 +13,11 @@ pub type LanguageMapping = HashMap<String, Language>;
pub struct Config { pub struct Config {
width: u32, width: u32,
height: u32, height: u32,
scroll_speed: i32,
menu_height: u16, menu_height: u16,
editor_config: EditorConfig, editor_config: EditorConfig,
theme: Theme, theme: Theme,
extensions_mapping: LanguageMapping, extensions_mapping: LanguageMapping,
scroll: ScrollConfig,
} }
impl Config { impl Config {
@ -31,18 +32,14 @@ impl Config {
Self { Self {
width: 1024, width: 1024,
height: 860, height: 860,
scroll_speed: 10,
menu_height: 60, menu_height: 60,
theme: Theme::load(editor_config.current_theme().clone()), theme: Theme::load(editor_config.current_theme().clone()),
editor_config, editor_config,
extensions_mapping, extensions_mapping,
scroll: ScrollConfig::new(),
} }
} }
pub fn scroll_speed(&self) -> i32 {
self.scroll_speed
}
pub fn width(&self) -> u32 { pub fn width(&self) -> u32 {
self.width self.width
} }
@ -82,6 +79,14 @@ impl Config {
pub fn extensions_mapping(&self) -> &LanguageMapping { pub fn extensions_mapping(&self) -> &LanguageMapping {
&self.extensions_mapping &self.extensions_mapping
} }
pub fn scroll(&self) -> &ScrollConfig {
&self.scroll
}
pub fn scroll_mut(&mut self) -> &mut ScrollConfig {
&mut self.scroll
}
} }
#[cfg(test)] #[cfg(test)]
@ -111,4 +116,23 @@ mod tests {
assert_eq!(keys, expected); assert_eq!(keys, expected);
} }
} }
#[test]
fn assert_scroll() {
let config = Config::new();
let result = config.scroll();
let expected = ScrollConfig::new();
assert_eq!(result.clone(), expected);
}
#[test]
fn assert_scroll_mut() {
let mut config = Config::new();
let result = config.scroll_mut();
result.set_margin_right(1236);
let mut expected = ScrollConfig::new();
expected.set_margin_right(1236);
assert_eq!(result.clone(), expected);
}
} }

View File

@ -4,10 +4,12 @@ pub mod config;
pub(crate) mod creator; pub(crate) mod creator;
pub mod directories; pub mod directories;
pub mod editor_config; pub mod editor_config;
pub mod scroll_config;
pub use crate::config::config::*; pub use crate::config::config::*;
pub use crate::config::directories::*; pub use crate::config::directories::*;
pub use crate::config::editor_config::*; pub use crate::config::editor_config::*;
pub use crate::config::scroll_config::*;
pub type ConfigAccess = Arc<RwLock<Config>>; pub type ConfigAccess = Arc<RwLock<Config>>;

View File

@ -0,0 +1,96 @@
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScrollConfig {
width: u32,
margin_right: i32,
speed: i32,
}
impl ScrollConfig {
pub fn new() -> Self {
Self {
width: 4,
margin_right: 5,
speed: 10,
}
}
pub fn width(&self) -> u32 {
self.width
}
pub fn set_width(&mut self, width: u32) {
self.width = width;
}
pub fn margin_right(&self) -> i32 {
self.margin_right
}
pub fn set_margin_right(&mut self, margin_right: i32) {
self.margin_right = margin_right;
}
pub fn speed(&self) -> i32 {
self.speed
}
pub fn set_speed(&mut self, speed: i32) {
self.speed = speed
}
}
mod tests {
use super::*;
#[test]
fn assert_width() {
let config = ScrollConfig::new();
let result = config.width();
let expected = 4;
assert_eq!(result, expected);
}
#[test]
fn assert_set_width() {
let mut config = ScrollConfig::new();
config.set_width(60);
let result = config.width();
let expected = 60;
assert_eq!(result, expected);
}
#[test]
fn assert_margin_right() {
let config = ScrollConfig::new();
let result = config.margin_right();
let expected = 5;
assert_eq!(result, expected);
}
#[test]
fn assert_set_margin_right() {
let mut config = ScrollConfig::new();
config.set_margin_right(98);
let result = config.margin_right();
let expected = 98;
assert_eq!(result, expected);
}
#[test]
fn assert_speed() {
let config = ScrollConfig::new();
let result = config.speed();
let expected = 10;
assert_eq!(result, expected);
}
#[test]
fn assert_set_speed() {
let mut config = ScrollConfig::new();
config.set_speed(98);
let result = config.speed();
let expected = 98;
assert_eq!(result, expected);
}
}

View File

@ -71,10 +71,13 @@ impl Deref for Caret {
} }
impl Render for Caret { impl Render for Caret {
fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, parent: Parent) { fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, context: &RenderContext) {
let dest = match parent { use std::borrow::*;
Some(parent) => move_render_point(parent.render_start_point(), self.dest()), use std::option::*;
None => self.dest().clone(),
let dest = match context.borrow() {
RenderContext::RelativePosition(p) => move_render_point(p.clone(), self.dest()),
_ => self.dest().clone(),
}; };
let start = Point::new(dest.x(), dest.y()); let start = Point::new(dest.x(), dest.y());
let end = Point::new(dest.x(), dest.y() + dest.height() as i32); let end = Point::new(dest.x(), dest.y() + dest.height() as i32);

View File

@ -32,17 +32,11 @@ impl EditorFile {
ext, ext,
Arc::clone(&config), Arc::clone(&config),
)]; )];
let render_position = {
let c = config.read().unwrap();
let x = c.editor_left_margin();
let y = c.editor_top_margin();
Rect::new(x, y, 0, 0)
};
Self { Self {
path, path,
sections, sections,
dest: render_position, dest: Rect::new(0, 0, 0, 0),
buffer, buffer,
config, config,
line_height: 0, line_height: 0,
@ -69,7 +63,20 @@ impl EditorFile {
&self.dest &self.dest
} }
pub fn get_character_at(&self, index: usize) -> Option<TextCharacter> { pub fn get_section_at_mut(&mut self, index: usize) -> Option<&mut EditorFileSection> {
self.sections.get_mut(index)
}
fn refresh_characters_position(&mut self) {
let mut current: Rect = Rect::new(0, 0, 0, 0);
for section in self.sections.iter_mut() {
section.update_positions(&mut current);
}
}
}
impl TextCollection for EditorFile {
fn get_character_at(&self, index: usize) -> Option<TextCharacter> {
for section in self.sections.iter() { for section in self.sections.iter() {
let character = section.get_character_at(index); let character = section.get_character_at(index);
if character.is_some() { if character.is_some() {
@ -79,7 +86,7 @@ impl EditorFile {
None None
} }
pub fn get_line(&self, line: &usize) -> Option<Vec<&TextCharacter>> { fn get_line(&self, line: &usize) -> Option<Vec<&TextCharacter>> {
let mut vec: Vec<&TextCharacter> = vec![]; let mut vec: Vec<&TextCharacter> = vec![];
for section in self.sections.iter() { for section in self.sections.iter() {
match section.get_line(line) { match section.get_line(line) {
@ -95,7 +102,7 @@ impl EditorFile {
} }
} }
pub fn get_last_at_line(&self, line: usize) -> Option<TextCharacter> { fn get_last_at_line(&self, line: usize) -> Option<TextCharacter> {
let mut current = None; let mut current = None;
for section in self.sections.iter() { for section in self.sections.iter() {
let c = section.get_last_at_line(line); let c = section.get_last_at_line(line);
@ -105,23 +112,12 @@ impl EditorFile {
} }
current current
} }
pub fn get_section_at_mut(&mut self, index: usize) -> Option<&mut EditorFileSection> {
self.sections.get_mut(index)
}
fn refresh_characters_position(&mut self) {
let mut current: Rect = Rect::new(0, 0, 0, 0);
for section in self.sections.iter_mut() {
section.update_positions(&mut current);
}
}
} }
impl Render for EditorFile { impl Render for EditorFile {
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) { fn render(&self, canvas: &mut WC, renderer: &mut Renderer, context: &RenderContext) {
for section in self.sections.iter() { for section in self.sections.iter() {
section.render(canvas, renderer, parent); section.render(canvas, renderer, context);
} }
} }
@ -156,7 +152,6 @@ impl ClickHandler for EditorFile {
} }
} }
if index >= 0 { if index >= 0 {
let context = UpdateContext::ParentPosition(self.render_start_point());
return self return self
.get_section_at_mut(index as usize) .get_section_at_mut(index as usize)
.unwrap() .unwrap()
@ -165,10 +160,9 @@ impl ClickHandler for EditorFile {
UR::NoOp UR::NoOp
} }
fn is_left_click_target(&self, point: &Point, _context: &UpdateContext) -> bool { fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool {
let context = UpdateContext::ParentPosition(self.render_start_point());
for section in self.sections.iter() { for section in self.sections.iter() {
if section.is_left_click_target(point, &context) { if section.is_left_click_target(point, context) {
return true; return true;
} }
} }
@ -176,6 +170,27 @@ impl ClickHandler for EditorFile {
} }
} }
impl TextWidget for EditorFile {
fn full_rect(&self) -> Rect {
let mut max_line_width = 0;
let mut height = 0;
for (index, section) in self.sections.iter().enumerate() {
let r = section.full_rect();
if index == 0 {
height = r.height();
max_line_width = r.width();
} else {
height += r.height();
if max_line_width < r.width() {
max_line_width = r.width();
}
}
}
Rect::new(0, 0, max_line_width, height)
}
}
impl RenderBox for EditorFile { impl RenderBox for EditorFile {
fn render_start_point(&self) -> Point { fn render_start_point(&self) -> Point {
self.dest.top_left() self.dest.top_left()
@ -185,3 +200,37 @@ impl RenderBox for EditorFile {
&self.dest &self.dest
} }
} }
#[cfg(test)]
mod test_render_box {
use crate::app::*;
use crate::tests::support;
use crate::ui::*;
use sdl2::rect::*;
use sdl2::*;
use std::borrow::*;
use std::rc::*;
use std::sync::*;
#[test]
fn assert_dest() {
let config = support::build_config();
let buffer = "".to_owned();
let path = "/example.txt".to_owned();
let widget = EditorFile::new(path, buffer, config);
let result = widget.dest().clone();
let expected = Rect::new(0, 0, 1, 1);
assert_eq!(result, expected);
}
#[test]
fn assert_render_start_point() {
let config = support::build_config();
let buffer = "".to_owned();
let path = "/example.txt".to_owned();
let widget = EditorFile::new(path, buffer, config);
let result = widget.render_start_point().clone();
let expected = Point::new(0, 0);
assert_eq!(result, expected);
}
}

View File

@ -59,8 +59,36 @@ impl EditorFileSection {
c.update_position(current); c.update_position(current);
} }
} }
}
pub fn get_character_at(&self, index: usize) -> Option<TextCharacter> { impl TextWidget for EditorFileSection {
fn full_rect(&self) -> Rect {
let mut current_line_width = 0;
let mut max_line_width = 0;
let mut height = 0;
for (index, token) in self.tokens.iter().enumerate() {
let r = token.full_rect();
if index == 0 {
height = r.height();
current_line_width = r.width();
max_line_width = r.width();
} else if token.is_new_line() {
height += r.height();
if max_line_width < current_line_width {
max_line_width = current_line_width;
}
current_line_width = 0;
} else {
current_line_width += r.width();
}
}
Rect::new(0, 0, max_line_width, height)
}
}
impl TextCollection for EditorFileSection {
fn get_character_at(&self, index: usize) -> Option<TextCharacter> {
for token in self.tokens.iter() { for token in self.tokens.iter() {
let character = token.get_character_at(index); let character = token.get_character_at(index);
if character.is_some() { if character.is_some() {
@ -70,7 +98,7 @@ impl EditorFileSection {
None None
} }
pub fn get_line(&self, line: &usize) -> Option<Vec<&TextCharacter>> { fn get_line(&self, line: &usize) -> Option<Vec<&TextCharacter>> {
let mut vec: Vec<&TextCharacter> = vec![]; let mut vec: Vec<&TextCharacter> = vec![];
for token in self.tokens.iter() { for token in self.tokens.iter() {
match token.get_line(line) { match token.get_line(line) {
@ -85,7 +113,7 @@ impl EditorFileSection {
} }
} }
pub fn get_last_at_line(&self, line: usize) -> Option<TextCharacter> { fn get_last_at_line(&self, line: usize) -> Option<TextCharacter> {
let mut current: Option<TextCharacter> = None; let mut current: Option<TextCharacter> = None;
for token in self.tokens.iter() { for token in self.tokens.iter() {
if !token.is_last_in_line() { if !token.is_last_in_line() {
@ -101,9 +129,9 @@ impl EditorFileSection {
} }
impl Render for EditorFileSection { impl Render for EditorFileSection {
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) { fn render(&self, canvas: &mut WC, renderer: &mut Renderer, context: &RenderContext) {
for token in self.tokens.iter() { for token in self.tokens.iter() {
token.render(canvas, renderer, parent); token.render(canvas, renderer, context);
} }
} }

View File

@ -50,13 +50,35 @@ impl EditorFileToken {
self.last_in_line self.last_in_line
} }
pub fn is_new_line(&self) -> bool {
self.token_type.is_new_line()
}
pub fn update_position(&mut self, current: &mut Rect) { pub fn update_position(&mut self, current: &mut Rect) {
for text_character in self.characters.iter_mut() { for text_character in self.characters.iter_mut() {
text_character.update_position(current); text_character.update_position(current);
} }
} }
}
pub fn get_character_at(&self, index: usize) -> Option<TextCharacter> { impl TextWidget for EditorFileToken {
fn full_rect(&self) -> Rect {
let mut rect = Rect::new(0, 0, 0, 0);
match self.characters.first() {
Some(c) => {
rect.set_x(c.dest().x());
rect.set_y(c.dest().y());
rect.set_width(c.dest().width());
rect.set_height(c.dest().height());
}
_ => return rect,
};
rect
}
}
impl TextCollection for EditorFileToken {
fn get_character_at(&self, index: usize) -> Option<TextCharacter> {
for character in self.characters.iter() { for character in self.characters.iter() {
if character.position() == index { if character.position() == index {
return Some(character.clone()); return Some(character.clone());
@ -65,7 +87,7 @@ impl EditorFileToken {
None None
} }
pub fn get_line(&self, line: &usize) -> Option<Vec<&TextCharacter>> { fn get_line(&self, line: &usize) -> Option<Vec<&TextCharacter>> {
let mut vec: Vec<&TextCharacter> = vec![]; let mut vec: Vec<&TextCharacter> = vec![];
for c in self.characters.iter() { for c in self.characters.iter() {
match ( match (
@ -93,7 +115,7 @@ impl EditorFileToken {
} }
} }
pub fn get_last_at_line(&self, line: usize) -> Option<TextCharacter> { fn get_last_at_line(&self, line: usize) -> Option<TextCharacter> {
let mut current: Option<&TextCharacter> = None; let mut current: Option<&TextCharacter> = None;
for text_character in self.characters.iter() { for text_character in self.characters.iter() {
if !text_character.is_last_in_line() { if !text_character.is_last_in_line() {
@ -112,12 +134,12 @@ impl Render for EditorFileToken {
* Must first create targets so even if new line appear renderer will know * Must first create targets so even if new line appear renderer will know
* where move render starting point * where move render starting point
*/ */
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) { fn render(&self, canvas: &mut WC, renderer: &mut Renderer, context: &RenderContext) {
if self.token_type.is_new_line() { if self.token_type.is_new_line() {
return; return;
} }
for text_character in self.characters.iter() { for text_character in self.characters.iter() {
text_character.render(canvas, renderer, parent); text_character.render(canvas, renderer, context);
} }
} }

View File

@ -1,3 +1,5 @@
use sdl2::rect::Rect;
pub mod editor_file; pub mod editor_file;
pub mod editor_file_section; pub mod editor_file_section;
pub mod editor_file_token; pub mod editor_file_token;
@ -5,3 +7,16 @@ pub mod editor_file_token;
pub use crate::ui::file::editor_file::*; pub use crate::ui::file::editor_file::*;
pub use crate::ui::file::editor_file_section::*; pub use crate::ui::file::editor_file_section::*;
pub use crate::ui::file::editor_file_token::*; pub use crate::ui::file::editor_file_token::*;
use crate::ui::TextCharacter;
pub trait TextCollection {
fn get_character_at(&self, index: usize) -> Option<TextCharacter>;
fn get_line(&self, line: &usize) -> Option<Vec<&TextCharacter>>;
fn get_last_at_line(&self, line: usize) -> Option<TextCharacter>;
}
pub trait TextWidget {
fn full_rect(&self) -> Rect;
}

View File

@ -1,266 +0,0 @@
use sdl2::rect::*;
use std::borrow::*;
use std::mem;
use std::rc::Rc;
use std::sync::*;
use crate::app::*;
use crate::app::{UpdateResult as UR, WindowCanvas as WS};
use crate::config::*;
use crate::ui::*;
pub struct FileEditor {
dest: Rect,
scroll: Point,
caret: Caret,
file: Option<EditorFile>,
config: ConfigAccess,
}
impl FileEditor {
pub fn new(config: ConfigAccess) -> Self {
let dest = {
let c = config.read().unwrap();
Rect::new(
c.editor_left_margin(),
c.editor_top_margin(),
c.width() - c.editor_left_margin() as u32,
c.height() - c.editor_top_margin() as u32,
)
};
Self {
dest,
scroll: Point::new(0, 0),
caret: Caret::new(Arc::clone(&config)),
file: None,
config,
}
}
pub fn caret(&self) -> &Caret {
&self.caret
}
pub fn caret_mut(&mut self) -> &mut Caret {
&mut self.caret
}
pub fn has_file(&self) -> bool {
self.file.is_some()
}
pub fn drop_file(&mut self) -> Option<EditorFile> {
if self.has_file() {
let mut file = None;
mem::swap(&mut self.file, &mut file);
file
} else {
None
}
}
pub fn open_file(&mut self, file: EditorFile) -> Option<EditorFile> {
let mut file = Some(file);
mem::swap(&mut self.file, &mut file);
file
}
pub fn file(&self) -> Option<&EditorFile> {
self.file.as_ref()
}
pub fn file_mut(&mut self) -> Option<&mut EditorFile> {
self.file.as_mut()
}
pub fn move_caret(&mut self, dir: MoveDirection) {
match dir {
MoveDirection::Left => {}
MoveDirection::Right => caret_manager::move_caret_right(self),
MoveDirection::Up => {}
MoveDirection::Down => {}
}
}
pub fn delete_front(&mut self, renderer: &mut Renderer) {
file_content_manager::delete_front(self, renderer);
}
pub fn delete_back(&mut self, renderer: &mut Renderer) {
file_content_manager::delete_back(self, renderer);
}
pub fn insert_text(&mut self, text: String, renderer: &mut Renderer) {
file_content_manager::insert_text(self, text, renderer);
}
pub fn insert_new_line(&mut self, renderer: &mut Renderer) {
file_content_manager::insert_new_line(self, renderer);
}
pub fn replace_current_file(&mut self, file: EditorFile) {
self.open_file(file);
}
pub fn scroll_to(&mut self, x: i32, y: i32) {
let read_config = self.config.read().unwrap();
self.scroll = self.scroll
+ Point::new(
read_config.scroll_speed() * x,
read_config.scroll_speed() * y,
);
}
fn is_text_character_clicked(&self, point: &Point) -> bool {
let context = UpdateContext::ParentPosition(self.render_start_point());
self.file()
.map_or(false, |file| file.is_left_click_target(point, &context))
}
fn is_editor_clicked(&self, point: &Point) -> bool {
self.dest
.contains_point(move_render_point(point.clone(), &self.dest).top_left())
}
fn resolve_line_from_point(&self, point: &Point) -> i32 {
let file = match self.file() {
Some(f) => f,
_ => return 0,
};
let mut y = point.y() - self.render_start_point().y();
if y < 0 {
y = 0;
}
y / (file.line_height() as i32)
}
fn set_caret_to_end_of_line(&mut self, line: i32) {
let file = match self.file_mut() {
Some(f) => f,
_ => return,
};
let mut line = line;
while line >= 0 {
match file.get_last_at_line(line.clone() as usize) {
Some(text_character) => {
let rect = text_character.dest();
let position =
CaretPosition::new(text_character.position() + 1, line as usize, 0);
let p = if text_character.is_last_in_line() && text_character.is_new_line() {
rect.top_left()
} else {
rect.top_right()
};
self.caret.move_caret(position, p);
break;
}
_ => {
line -= 1;
}
}
}
}
}
impl Render for FileEditor {
fn render(&self, canvas: &mut WS, renderer: &mut Renderer, _parent: Parent) {
canvas.set_clip_rect(self.dest.clone());
match self.file() {
Some(file) => file.render(canvas, renderer, Some(self)),
_ => (),
};
self.caret.render(canvas, renderer, Some(self));
}
fn prepare_ui(&mut self, renderer: &mut Renderer) {
self.caret.prepare_ui(renderer);
}
}
impl Update for FileEditor {
fn update(&mut self, ticks: i32, context: &UpdateContext) -> UR {
{
let config = self.config.read().unwrap();
self.dest
.set_width(config.width() - config.editor_left_margin() as u32);
self.dest
.set_height(config.height() - config.editor_top_margin() as u32);
}
self.caret.update(ticks, context);
match self.file_mut() {
Some(file) => file.update(ticks, context),
_ => UR::NoOp,
}
}
}
impl ClickHandler for FileEditor {
fn on_left_click(&mut self, point: &Point, _context: &UpdateContext) -> UR {
let context = UpdateContext::ParentPosition(self.render_start_point());
if self.is_text_character_clicked(point) {
let file = if let Some(file) = self.file_mut() {
file
} else {
return UR::NoOp;
};
match file.on_left_click(point, &context) {
UR::MoveCaret(rect, position) => {
self.caret
.move_caret(position, Point::new(rect.x(), rect.y()));
}
_ => {}
}
} else {
self.set_caret_to_end_of_line(self.resolve_line_from_point(point));
}
UR::NoOp
}
fn is_left_click_target(&self, point: &Point, _context: &UpdateContext) -> bool {
self.is_text_character_clicked(point) || self.is_editor_clicked(point)
}
}
impl RenderBox for FileEditor {
fn render_start_point(&self) -> Point {
self.dest.top_left() + self.scroll
}
fn dest(&self) -> &Rect {
&self.dest
}
}
impl ConfigHolder for FileEditor {
fn config(&self) -> &ConfigAccess {
&self.config
}
}
#[cfg(test)]
mod tests {
use crate::app::*;
use crate::ui::*;
use sdl2::rect::*;
use sdl2::*;
use std::borrow::*;
use std::rc::*;
use std::sync::*;
#[test]
fn replace_file() {
let config = Arc::new(RwLock::new(Config::new()));
let mut editor = FileEditor::new(Arc::clone(&config));
let first_file =
EditorFile::new("./foo.txt".to_string(), "foo".to_string(), config.clone());
let second_file =
EditorFile::new("./bar.txt".to_string(), "bar".to_string(), config.clone());
editor.open_file(first_file.clone());
let result = editor.open_file(second_file.clone());
assert_eq!(result.is_some(), true);
let file = result.as_ref().unwrap();
assert_eq!(file.path(), first_file.path());
assert_eq!(file.buffer(), first_file.buffer());
}
}

View File

@ -0,0 +1,429 @@
use sdl2::pixels::*;
use sdl2::rect::*;
use std::borrow::*;
use std::mem;
use std::sync::*;
use crate::app::*;
use crate::app::{UpdateResult as UR, WindowCanvas as WS};
use crate::ui::scroll_bar::horizontal_scroll_bar::*;
use crate::ui::scroll_bar::vertical_scroll_bar::*;
use crate::ui::scroll_bar::Scrollable;
use crate::ui::*;
pub struct FileEditor {
dest: Rect,
full_rect: Rect,
caret: Caret,
file: Option<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(&mut self, renderer: &mut Renderer) {
file_content_manager::delete_front(self, renderer);
}
pub fn delete_back(&mut self, renderer: &mut Renderer) {
file_content_manager::delete_back(self, renderer);
}
pub fn insert_text(&mut self, text: String, renderer: &mut Renderer) {
file_content_manager::insert_text(self, text, renderer);
}
pub fn insert_new_line(&mut self, renderer: &mut Renderer) {
file_content_manager::insert_new_line(self, renderer);
}
fn is_text_character_clicked(&self, point: &Point) -> bool {
let context = UpdateContext::ParentPosition(self.render_start_point());
self.file()
.map_or(false, |file| file.is_left_click_target(point, &context))
}
fn is_editor_clicked(&self, point: &Point) -> bool {
self.dest
.contains_point(move_render_point(point.clone(), &self.dest).top_left())
}
fn resolve_line_from_point(&self, point: &Point) -> i32 {
let file = match self.file() {
Some(f) => f,
_ => return 0,
};
let mut y = point.y() - self.render_start_point().y();
if y < 0 {
y = 0;
}
y / (file.line_height() as i32)
}
}
impl ScrollableView for FileEditor {
fn scroll_to(&mut self, x: i32, y: i32) {
let read_config = self.config.read().unwrap();
let value_x = read_config.scroll().speed() * x;
let value_y = read_config.scroll().speed() * y;
let old_x = self.horizontal_scroll_bar.scroll_value();
let old_y = self.vertical_scroll_bar.scroll_value();
if value_x + old_x >= 0 {
self.horizontal_scroll_bar.scroll_to(value_x + old_x);
if self.horizontal_scroll_bar.scrolled_part() > 1.0 {
self.horizontal_scroll_bar.scroll_to(old_x);
}
}
if value_y + old_y >= 0 {
self.vertical_scroll_bar.scroll_to(value_y + old_y);
if self.vertical_scroll_bar.scrolled_part() > 1.0 {
self.vertical_scroll_bar.scroll_to(old_y);
}
}
}
fn scroll(&self) -> Point {
Point::new(
-self.horizontal_scroll_bar.scroll_value(),
-self.vertical_scroll_bar.scroll_value(),
)
}
}
impl FileAccess for FileEditor {
fn has_file(&self) -> bool {
self.file.is_some()
}
fn file(&self) -> Option<&EditorFile> {
self.file.as_ref()
}
fn file_mut(&mut self) -> Option<&mut EditorFile> {
self.file.as_mut()
}
fn open_file(&mut self, file: EditorFile) -> Option<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();
}
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 => {}
MoveDirection::Right => caret_manager::move_caret_right(self),
MoveDirection::Up => {}
MoveDirection::Down => {}
}
}
fn set_caret_to_end_of_line(&mut self, line: i32) {
let file = match self.file_mut() {
Some(f) => f,
_ => return,
};
let mut line = line;
while line >= 0 {
match file.get_last_at_line(line.clone() as usize) {
Some(text_character) => {
let rect = text_character.dest();
let position =
CaretPosition::new(text_character.position() + 1, line as usize, 0);
let p = if text_character.is_last_in_line() && text_character.is_new_line() {
rect.top_left()
} else {
rect.top_right()
};
self.caret.move_caret(position, p);
break;
}
_ => {
line -= 1;
}
}
}
}
}
impl Render for FileEditor {
fn render(&self, canvas: &mut WS, renderer: &mut Renderer, _context: &RenderContext) {
canvas.set_clip_rect(self.dest.clone());
match self.file() {
Some(file) => file.render(
canvas,
renderer,
&RenderContext::RelativePosition(self.render_start_point()),
),
_ => (),
};
self.caret.render(
canvas,
renderer,
&RenderContext::RelativePosition(self.render_start_point()),
);
self.vertical_scroll_bar.render(
canvas,
renderer,
&RenderContext::RelativePosition(self.dest.top_left()),
);
self.horizontal_scroll_bar.render(
canvas,
renderer,
&RenderContext::RelativePosition(self.dest.top_left()),
);
}
fn prepare_ui(&mut self, renderer: &mut Renderer) {
self.caret.prepare_ui(renderer);
}
}
impl Update for FileEditor {
fn update(&mut self, ticks: i32, context: &UpdateContext) -> UR {
let (width, height, editor_left_margin, editor_top_margin, scroll_width, scroll_margin) = {
let config: RwLockReadGuard<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(ticks, context);
match self.file_mut() {
Some(file) => file.update(ticks, context),
_ => UR::NoOp,
}
}
}
impl ClickHandler for FileEditor {
fn on_left_click(&mut self, point: &Point, _context: &UpdateContext) -> UR {
let context = UpdateContext::ParentPosition(self.render_start_point());
if self.is_text_character_clicked(point) {
let file = if let Some(file) = self.file_mut() {
file
} else {
return UR::NoOp;
};
match file.on_left_click(point, &context) {
UR::MoveCaret(rect, position) => {
self.caret
.move_caret(position, Point::new(rect.x(), rect.y()));
}
_ => {}
}
} else {
self.set_caret_to_end_of_line(self.resolve_line_from_point(point));
}
UR::NoOp
}
fn is_left_click_target(&self, point: &Point, _context: &UpdateContext) -> bool {
self.is_text_character_clicked(point) || self.is_editor_clicked(point)
}
}
impl RenderBox for FileEditor {
fn render_start_point(&self) -> Point {
self.dest.top_left() + self.scroll()
}
fn dest(&self) -> &Rect {
&self.dest
}
}
impl ConfigHolder for FileEditor {
fn config(&self) -> &ConfigAccess {
&self.config
}
}
#[cfg(test)]
mod tests {
use crate::app::*;
use crate::ui::*;
use sdl2::rect::*;
use sdl2::*;
use std::borrow::*;
use std::rc::*;
use std::sync::*;
#[test]
fn replace_file() {
let config = Arc::new(RwLock::new(Config::new()));
let mut editor = FileEditor::new(Arc::clone(&config));
let first_file =
EditorFile::new("./foo.txt".to_string(), "foo".to_string(), config.clone());
let second_file =
EditorFile::new("./bar.txt".to_string(), "bar".to_string(), config.clone());
editor.open_file(first_file.clone());
let result = editor.open_file(second_file.clone());
assert_eq!(result.is_some(), true);
let file = result.as_ref().unwrap();
assert_eq!(file.path(), first_file.path());
assert_eq!(file.buffer(), first_file.buffer());
}
}
#[cfg(test)]
mod test_config_holder {
use crate::app::*;
use crate::tests::support;
use crate::ui::*;
use sdl2::rect::*;
use sdl2::*;
use std::borrow::*;
use std::rc::*;
use std::sync::*;
#[test]
fn assert_config() {
let config = support::build_config();
let widget = FileEditor::new(Arc::clone(&config));
let result = widget.config();
{
let mut w = config.write().unwrap();
w.set_height(1240);
w.set_width(1024);
}
let local = config.read().unwrap();
let widget_config = result.read().unwrap();
assert_eq!(widget_config.width(), local.width());
assert_eq!(widget_config.height(), local.height());
}
}
#[cfg(test)]
mod test_render_box {
use crate::app::*;
use crate::tests::support;
use crate::ui::*;
use sdl2::rect::*;
use sdl2::*;
use std::borrow::*;
use std::rc::*;
use std::sync::*;
impl FileEditor {
pub fn set_full_rect(&mut self, r: Rect) {
self.full_rect = r;
}
pub fn set_dest(&mut self, r: Rect) {
self.dest = r;
}
}
#[test]
fn assert_dest() {
let config = support::build_config();
let (x, y, mw, mh) = {
let c = config.read().unwrap();
(
c.editor_left_margin(),
c.editor_top_margin(),
c.width(),
c.height(),
)
};
let widget = FileEditor::new(config);
let result = widget.dest().clone();
let expected = Rect::new(x, y, mw - x as u32, mh - y as u32);
assert_eq!(result, expected);
}
#[test]
fn assert_render_start_point() {
let config = support::build_config();
let (x, y, ss) = {
let c = config.read().unwrap();
(
c.editor_left_margin(),
c.editor_top_margin(),
c.scroll().speed(),
)
};
let mut widget = FileEditor::new(config);
widget.set_dest(Rect::new(x.clone(), y.clone(), 999, 999));
widget.set_full_rect(Rect::new(0, 0, 99999, 99999));
widget.update(1, &UpdateContext::Nothing);
widget.scroll_to(30, 40);
let result = widget.render_start_point().clone();
let expected = Point::new(x - (ss * 30), y - (ss * 40));
assert_eq!(result, expected);
}
}

36
src/ui/file_editor/mod.rs Normal file
View File

@ -0,0 +1,36 @@
use crate::ui::*;
use sdl2::rect::*;
pub mod file_editor;
pub use crate::ui::file_editor::file_editor::*;
pub trait FileAccess {
fn has_file(&self) -> bool;
fn file(&self) -> Option<&EditorFile>;
fn file_mut(&mut self) -> Option<&mut EditorFile>;
fn open_file(&mut self, file: EditorFile) -> Option<EditorFile>;
fn drop_file(&mut self) -> Option<EditorFile>;
fn replace_current_file(&mut self, file: EditorFile);
}
pub trait CaretAccess {
fn caret(&self) -> &Caret;
fn caret_mut(&mut self) -> &mut Caret;
fn move_caret(&mut self, dir: MoveDirection);
fn set_caret_to_end_of_line(&mut self, line: i32);
}
pub trait ScrollableView {
fn scroll_to(&mut self, x: i32, y: i32);
fn scroll(&self) -> Point;
}

View File

@ -34,21 +34,23 @@ impl MenuBar {
} }
impl Render for MenuBar { impl Render for MenuBar {
fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, parent: Parent) { fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, context: &RenderContext) {
use std::borrow::*;
canvas.set_clip_rect(self.dest.clone()); canvas.set_clip_rect(self.dest.clone());
canvas.set_draw_color(self.background_color.clone()); canvas.set_draw_color(self.background_color.clone());
canvas canvas
.fill_rect(match parent { .fill_rect(match context.borrow() {
None => self.dest.clone(), RenderContext::RelativePosition(p) => move_render_point(p.clone(), self.dest()),
Some(parent) => move_render_point(parent.render_start_point(), self.dest()), _ => self.dest.clone(),
}) })
.unwrap_or_else(|_| panic!("Failed to draw main menu background")); .unwrap_or_else(|_| panic!("Failed to draw main menu background"));
canvas.set_draw_color(self.border_color.clone()); canvas.set_draw_color(self.border_color.clone());
canvas canvas
.draw_rect(match parent { .draw_rect(match context.borrow() {
None => self.dest.clone(), RenderContext::RelativePosition(p) => move_render_point(p.clone(), self.dest()),
Some(parent) => move_render_point(parent.render_start_point(), self.dest()), _ => self.dest.clone(),
}) })
.unwrap_or_else(|_| panic!("Failed to draw main menu background")); .unwrap_or_else(|_| panic!("Failed to draw main menu background"));
} }

View File

@ -10,6 +10,7 @@ pub mod file;
pub mod file_editor; pub mod file_editor;
pub mod menu_bar; pub mod menu_bar;
pub mod project_tree; pub mod project_tree;
pub mod scroll_bar;
pub mod text_character; pub mod text_character;
pub use crate::ui::caret::*; pub use crate::ui::caret::*;
@ -17,17 +18,22 @@ pub use crate::ui::file::*;
pub use crate::ui::file_editor::*; pub use crate::ui::file_editor::*;
pub use crate::ui::menu_bar::*; pub use crate::ui::menu_bar::*;
pub use crate::ui::project_tree::*; pub use crate::ui::project_tree::*;
pub use crate::ui::scroll_bar::*;
pub use crate::ui::text_character::*; pub use crate::ui::text_character::*;
pub type Parent<'l> = Option<&'l RenderBox>; #[derive(Debug)]
pub type ParentMut<'l> = Option<&'l mut RenderBox>;
pub enum UpdateContext<'l> { pub enum UpdateContext<'l> {
Nothing, Nothing,
ParentPosition(Point), ParentPosition(Point),
CurrentFile(&'l mut EditorFile), CurrentFile(&'l mut EditorFile),
} }
#[derive(Clone, PartialEq, Debug)]
pub enum RenderContext {
Nothing,
RelativePosition(Point),
}
#[inline] #[inline]
pub fn is_in_rect(point: &Point, rect: &Rect) -> bool { pub fn is_in_rect(point: &Point, rect: &Rect) -> bool {
rect.contains_point(point.clone()) rect.contains_point(point.clone())
@ -64,7 +70,7 @@ pub fn move_render_point(p: Point, d: &Rect) -> Rect {
} }
pub trait Render { pub trait Render {
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent); fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: &RenderContext);
fn prepare_ui(&mut self, renderer: &mut Renderer); fn prepare_ui(&mut self, renderer: &mut Renderer);
} }

View File

@ -0,0 +1,203 @@
use crate::app::{UpdateResult as UR, WindowCanvas as WC};
use crate::config::*;
use crate::renderer::*;
use crate::ui::*;
use sdl2::pixels::*;
use sdl2::rect::*;
pub struct HorizontalScrollBar {
scroll_value: i32,
viewport: u32,
full_width: u32,
rect: Rect,
}
impl HorizontalScrollBar {
pub fn new(config: ConfigAccess) -> Self {
let width = { config.read().unwrap().scroll().width() };
Self {
scroll_value: 0,
viewport: 1,
full_width: 1,
rect: Rect::new(0, 0, width, 0),
}
}
#[inline]
pub fn viewport(&self) -> u32 {
self.viewport
}
#[inline]
pub fn full_width(&self) -> u32 {
self.full_width
}
#[inline]
pub fn rect(&self) -> &Rect {
&self.rect
}
}
impl Update for HorizontalScrollBar {
fn update(&mut self, _ticks: i32, _context: &UpdateContext) -> UR {
if self.full_width < self.viewport {
return UR::NoOp;
}
let ratio = self.full_width as f64 / self.viewport as f64;
self.rect.set_width((self.viewport as f64 / ratio) as u32);
let x = (self.viewport - self.rect.width()) as f64
* (self.scroll_value().abs() as f64 / (self.full_width - self.viewport) as f64);
self.rect.set_x(x as i32);
UR::NoOp
}
}
impl Render for HorizontalScrollBar {
fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, context: &RenderContext) {
if self.full_width < self.viewport {
return;
}
canvas.set_draw_color(Color::RGBA(255, 255, 255, 0));
canvas
.fill_rect(match context {
RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.rect),
_ => self.rect.clone(),
})
.unwrap_or_else(|_| panic!("Failed to render vertical scroll back"));
}
fn prepare_ui(&mut self, _renderer: &mut Renderer) {}
}
impl Scrollable for HorizontalScrollBar {
fn scroll_to(&mut self, n: i32) {
self.scroll_value = n;
}
fn scroll_value(&self) -> i32 {
self.scroll_value
}
fn set_viewport(&mut self, n: u32) {
self.viewport = n;
}
fn set_full_size(&mut self, n: u32) {
self.full_width = n;
}
fn set_location(&mut self, n: i32) {
self.rect.set_y(n);
}
fn scrolled_part(&self) -> f64 {
if self.full_width() < self.viewport() {
return 1.0;
}
self.scroll_value().abs() as f64 / (self.full_width() - self.viewport()) as f64
}
}
#[cfg(test)]
mod test_update {
use super::*;
use crate::tests::support;
use std::sync::*;
impl HorizontalScrollBar {
pub fn rect_mut(&mut self) -> &mut Rect {
&mut self.rect
}
}
#[test]
fn assert_do_nothing_when_small_content() {
let config = support::build_config();
let mut widget = HorizontalScrollBar::new(Arc::clone(&config));
widget.set_viewport(100);
widget.set_full_size(20);
widget.rect_mut().set_x(30000000);
widget.rect_mut().set_width(30000000);
widget.update(0, &UpdateContext::Nothing);
assert_eq!(widget.rect().x(), 30000000);
assert_eq!(widget.rect().width(), 30000000);
}
#[test]
fn assert_update_when_huge_content() {
let config = support::build_config();
let mut widget = HorizontalScrollBar::new(Arc::clone(&config));
widget.set_viewport(100);
widget.set_full_size(200);
widget.rect_mut().set_x(30000000);
widget.rect_mut().set_width(30000000);
widget.update(0, &UpdateContext::Nothing);
assert_eq!(widget.rect().x(), 0);
assert_eq!(widget.rect().width(), 50);
}
}
#[cfg(test)]
mod test_scrollable {
use super::*;
use crate::tests::support;
use std::sync::*;
#[test]
fn assert_scroll_to() {
let config = support::build_config();
let mut widget = HorizontalScrollBar::new(Arc::clone(&config));
let old = widget.scroll_value();
widget.scroll_to(157);
let current = widget.scroll_value();
let expected = 157;
assert_ne!(old, current);
assert_eq!(current, expected);
}
#[test]
fn assert_scroll_value() {
let config = support::build_config();
let widget = HorizontalScrollBar::new(Arc::clone(&config));
assert_eq!(widget.scroll_value(), 0);
}
#[test]
fn assert_set_viewport() {
let config = support::build_config();
let mut widget = HorizontalScrollBar::new(Arc::clone(&config));
let old = widget.viewport();
widget.set_viewport(157);
let current = widget.viewport();
let expected = 157;
assert_ne!(old, current);
assert_eq!(current, expected);
}
#[test]
fn assert_set_full_size() {
let config = support::build_config();
let mut widget = HorizontalScrollBar::new(Arc::clone(&config));
let old = widget.full_width();
widget.set_full_size(157);
let current = widget.full_width();
let expected = 157;
assert_ne!(old, current);
assert_eq!(current, expected);
}
#[test]
fn assert_set_location() {
let config = support::build_config();
let mut widget = HorizontalScrollBar::new(Arc::clone(&config));
let old = widget.rect().y();
widget.set_location(157);
let current = widget.rect().y();
let expected = 157;
assert_ne!(old, current);
assert_eq!(current, expected);
}
}

19
src/ui/scroll_bar/mod.rs Normal file
View File

@ -0,0 +1,19 @@
pub mod horizontal_scroll_bar;
pub mod vertical_scroll_bar;
use crate::ui::scroll_bar::horizontal_scroll_bar::*;
use crate::ui::scroll_bar::vertical_scroll_bar::*;
pub trait Scrollable {
fn scroll_to(&mut self, n: i32);
fn scroll_value(&self) -> i32;
fn set_viewport(&mut self, n: u32);
fn set_full_size(&mut self, n: u32);
fn set_location(&mut self, n: i32);
fn scrolled_part(&self) -> f64;
}

View File

@ -0,0 +1,200 @@
use crate::app::{UpdateResult as UR, WindowCanvas as WC};
use crate::config::*;
use crate::renderer::*;
use crate::ui::*;
use sdl2::pixels::*;
use sdl2::rect::*;
pub struct VerticalScrollBar {
scroll_value: i32,
viewport: u32,
full_height: u32,
rect: Rect,
}
impl VerticalScrollBar {
pub fn new(config: ConfigAccess) -> Self {
let width = { config.read().unwrap().scroll().width() };
Self {
scroll_value: 0,
viewport: 1,
full_height: 1,
rect: Rect::new(0, 0, width, 0),
}
}
#[inline]
pub fn viewport(&self) -> u32 {
self.viewport
}
#[inline]
pub fn full_height(&self) -> u32 {
self.full_height
}
#[inline]
pub fn rect(&self) -> &Rect {
&self.rect
}
}
impl Update for VerticalScrollBar {
fn update(&mut self, _ticks: i32, _context: &UpdateContext) -> UR {
if self.full_height() < self.viewport() {
return UR::NoOp;
}
let ratio = self.full_height() as f64 / self.viewport() as f64;
self.rect
.set_height((self.viewport() as f64 / ratio) as u32);
let y = (self.viewport() - self.rect.height()) as f64 * self.scrolled_part();
self.rect.set_y(y as i32);
UR::NoOp
}
}
impl Render for VerticalScrollBar {
fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, context: &RenderContext) {
if self.full_height() < self.viewport() {
return;
}
canvas.set_draw_color(Color::RGBA(255, 255, 255, 0));
canvas
.fill_rect(match context {
RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.rect),
_ => self.rect.clone(),
})
.unwrap_or_else(|_| panic!("Failed to render vertical scroll back"));
}
fn prepare_ui(&mut self, _renderer: &mut Renderer) {}
}
impl Scrollable for VerticalScrollBar {
fn scroll_to(&mut self, n: i32) {
self.scroll_value = n;
}
fn scroll_value(&self) -> i32 {
self.scroll_value
}
fn set_viewport(&mut self, n: u32) {
self.viewport = n;
}
fn set_full_size(&mut self, n: u32) {
self.full_height = n;
}
fn set_location(&mut self, n: i32) {
self.rect.set_x(n);
}
fn scrolled_part(&self) -> f64 {
self.scroll_value().abs() as f64 / (self.full_height() - self.viewport()) as f64
}
}
#[cfg(test)]
mod test_update {
use super::*;
use crate::tests::support;
use std::sync::*;
impl VerticalScrollBar {
pub fn rect_mut(&mut self) -> &mut Rect {
&mut self.rect
}
}
#[test]
fn assert_do_nothing_when_small_content() {
let config = support::build_config();
let mut widget = VerticalScrollBar::new(Arc::clone(&config));
widget.set_viewport(100);
widget.set_full_size(20);
widget.rect_mut().set_y(30000000);
widget.rect_mut().set_height(30000000);
widget.update(0, &UpdateContext::Nothing);
assert_eq!(widget.rect().y(), 30000000);
assert_eq!(widget.rect().height(), 30000000);
}
#[test]
fn assert_update_when_huge_content() {
let config = support::build_config();
let mut widget = VerticalScrollBar::new(Arc::clone(&config));
widget.set_viewport(100);
widget.set_full_size(200);
widget.rect_mut().set_y(30000000);
widget.rect_mut().set_height(30000000);
widget.update(0, &UpdateContext::Nothing);
assert_eq!(widget.rect().y(), 0);
assert_eq!(widget.rect().height(), 50);
}
}
#[cfg(test)]
mod test_scrollable {
use super::*;
use crate::tests::support;
use std::sync::*;
#[test]
fn assert_scroll_to() {
let config = support::build_config();
let mut widget = VerticalScrollBar::new(Arc::clone(&config));
let old = widget.scroll_value();
widget.scroll_to(157);
let current = widget.scroll_value();
let expected = 157;
assert_ne!(old, current);
assert_eq!(current, expected);
}
#[test]
fn assert_scroll_value() {
let config = support::build_config();
let widget = VerticalScrollBar::new(Arc::clone(&config));
assert_eq!(widget.scroll_value(), 0);
}
#[test]
fn assert_set_viewport() {
let config = support::build_config();
let mut widget = VerticalScrollBar::new(Arc::clone(&config));
let old = widget.viewport();
widget.set_viewport(157);
let current = widget.viewport();
let expected = 157;
assert_ne!(old, current);
assert_eq!(current, expected);
}
#[test]
fn assert_set_full_size() {
let config = support::build_config();
let mut widget = VerticalScrollBar::new(Arc::clone(&config));
let old = widget.full_height();
widget.set_full_size(157);
let current = widget.full_height();
let expected = 157;
assert_ne!(old, current);
assert_eq!(current, expected);
}
#[test]
fn assert_set_location() {
let config = support::build_config();
let mut widget = VerticalScrollBar::new(Arc::clone(&config));
let old = widget.rect().x();
widget.set_location(157);
let current = widget.rect().x();
let expected = 157;
assert_ne!(old, current);
assert_eq!(current, expected);
}
}

View File

@ -105,7 +105,7 @@ impl Render for TextCharacter {
* Must first create targets so even if new line appear renderer will know * Must first create targets so even if new line appear renderer will know
* where move render starting point * where move render starting point
*/ */
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) { fn render(&self, canvas: &mut WC, renderer: &mut Renderer, context: &RenderContext) {
if self.is_new_line() { if self.is_new_line() {
return; return;
} }
@ -126,9 +126,9 @@ impl Render for TextCharacter {
color: self.color.clone(), color: self.color.clone(),
font: font_details.clone(), font: font_details.clone(),
}; };
let dest = match parent { let dest = match context {
None => self.dest.clone(), RenderContext::RelativePosition(p) => move_render_point(p.clone(), self.dest()),
Some(parent) => move_render_point(parent.render_start_point(), self.dest()), _ => self.dest.clone(),
}; };
if let Ok(texture) = renderer.texture_manager().load_text(&mut details, &font) { if let Ok(texture) = renderer.texture_manager().load_text(&mut details, &font) {
canvas canvas