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:
parent
4e13b4d2d7
commit
0380e3c9fd
15
README.md
15
README.md
@ -1,6 +1,17 @@
|
|||||||
|
[![codecov](https://codecov.io/gh/Eraden/rider/branch/master/graph/badge.svg)](https://codecov.io/gh/Eraden/rider)
|
||||||
|
[![CircleCI](https://circleci.com/gh/Eraden/rider.svg?style=svg&circle-token=546aae50b559665bd1f77a6452eff25e26a9d966)](https://circleci.com/gh/Eraden/rider)
|
||||||
|
|
||||||
# rider
|
# 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
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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>>;
|
||||||
|
|
||||||
|
96
src/config/scroll_config.rs
Normal file
96
src/config/scroll_config.rs
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
429
src/ui/file_editor/file_editor.rs
Normal file
429
src/ui/file_editor/file_editor.rs
Normal 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
36
src/ui/file_editor/mod.rs
Normal 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;
|
||||||
|
}
|
@ -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"));
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
203
src/ui/scroll_bar/horizontal_scroll_bar.rs
Normal file
203
src/ui/scroll_bar/horizontal_scroll_bar.rs
Normal 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
19
src/ui/scroll_bar/mod.rs
Normal 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;
|
||||||
|
}
|
200
src/ui/scroll_bar/vertical_scroll_bar.rs
Normal file
200
src/ui/scroll_bar/vertical_scroll_bar.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user