Add scrollbar (#1)

* Refactor code, prepare for scrollbar

* Move file to file editor, fix rust lexer new line, refactor code, fix caret render position

* Fix caret position at new lines

* Scroll content
This commit is contained in:
Adrian Woźniak 2019-01-06 18:25:20 +01:00 committed by GitHub
parent 1617598fd4
commit 7d6a5887fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1142 additions and 675 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
**/*.rs.bk
.idea
log
.rider

View File

@ -1,6 +1,6 @@
use crate::app::caret_manager;
use crate::app::file_content_manager;
use crate::app::{UpdateResult, WindowCanvas};
use crate::app::{UpdateResult, WindowCanvas as WC};
use crate::config::Config;
use crate::renderer::Renderer;
use crate::ui::caret::Caret;
@ -19,9 +19,8 @@ use std::sync::Arc;
pub struct AppState {
menu_bar: MenuBar,
files: Vec<EditorFile>,
current_file: usize,
caret: Caret,
config: Rc<Config>,
file_editor: FileEditor,
}
impl AppState {
@ -29,19 +28,29 @@ impl AppState {
Self {
menu_bar: MenuBar::new(config.clone()),
files: vec![],
current_file: 0,
caret: Caret::new(config.clone()),
file_editor: FileEditor::new(
Rect::new(
config.editor_left_margin(),
config.editor_top_margin(),
config.width() - config.editor_left_margin() as u32,
config.height() - config.editor_top_margin() as u32,
),
config.clone(),
),
config,
}
}
pub fn open_file(&mut self, file_path: String) {
pub fn open_file(&mut self, file_path: String, renderer: &mut Renderer) {
use std::fs::read_to_string;
if let Ok(buffer) = read_to_string(&file_path) {
let file = EditorFile::new(file_path.clone(), buffer, self.config.clone());
self.current_file = self.files.len();
self.files.push(file);
let mut file = EditorFile::new(file_path.clone(), buffer, self.config.clone());
file.prepare_ui(renderer);
match self.file_editor.open_file(file) {
Some(old) => self.files.push(old),
_ => (),
}
} else {
eprintln!("Failed to open file: {}", file_path);
};
@ -51,102 +60,55 @@ impl AppState {
&self.config
}
pub fn caret(&self) -> &Caret {
&self.caret
pub fn file_editor(&self) -> &FileEditor {
&self.file_editor
}
pub fn caret_mut(&mut self) -> &mut Caret {
&mut self.caret
}
pub fn current_file(&self) -> Option<&EditorFile> {
self.files.get(self.current_file)
}
pub fn current_file_mut(&mut self) -> Option<&mut EditorFile> {
self.files.get_mut(self.current_file)
}
fn on_editor_clicked(&mut self, point: &Point, video_subsystem: &mut VS) -> UpdateResult {
let current_file: &mut EditorFile = if let Some(current_file) = self.current_file_mut() {
current_file
} else {
return UpdateResult::NoOp;
};
if !current_file.is_left_click_target(point) {
return UpdateResult::NoOp;
}
video_subsystem.text_input().start();
match current_file.on_left_click(point) {
UpdateResult::MoveCaret(rect, position) => {
self.caret
.move_caret(position, Point::new(rect.x(), rect.y()));
}
_ => (),
};
UpdateResult::NoOp
}
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) {
file_content_manager::delete_front(self);
}
pub fn delete_back(&mut self) {
file_content_manager::delete_back(self);
}
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.files[self.current_file] = file;
pub fn file_editor_mut(&mut self) -> &mut FileEditor {
&mut self.file_editor
}
}
impl Render for AppState {
fn render(&mut self, canvas: &mut WindowCanvas, renderer: &mut Renderer) -> UpdateResult {
self.menu_bar.render(canvas, renderer);
if let Some(file) = self.current_file_mut() {
file.render(canvas, renderer);
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, _parent: Parent) -> UpdateResult {
self.file_editor.render(canvas, renderer, None);
self.menu_bar.render(canvas, renderer, None)
}
self.caret.render(canvas, renderer);
UpdateResult::NoOp
fn prepare_ui(&mut self, renderer: &mut Renderer) {
self.menu_bar.prepare_ui(renderer);
self.file_editor.prepare_ui(renderer);
}
}
impl Update for AppState {
fn update(&mut self, ticks: i32) -> UpdateResult {
self.menu_bar.update(ticks);
if let Some(file) = self.files.get_mut(self.current_file) {
file.update(ticks);
}
self.caret.update(ticks);
fn update(&mut self, ticks: i32, context: &UpdateContext) -> UpdateResult {
self.menu_bar.update(ticks, context);
self.file_editor.update(ticks, context);
UpdateResult::NoOp
}
}
impl AppState {
pub fn on_left_click(&mut self, point: &Point, video_subsystem: &mut VS) -> UpdateResult {
if self.menu_bar.is_left_click_target(point) {
if self
.menu_bar
.is_left_click_target(point, &UpdateContext::Nothing)
{
video_subsystem.text_input().stop();
return self.menu_bar.on_left_click(point);
return self.menu_bar.on_left_click(point, &UpdateContext::Nothing);
} else {
if !self
.file_editor
.is_left_click_target(point, &UpdateContext::Nothing)
{
return UpdateResult::NoOp;
} else {
video_subsystem.text_input().start();
self.file_editor
.on_left_click(point, &UpdateContext::Nothing);
}
}
self.on_editor_clicked(point, video_subsystem);
UpdateResult::NoOp
}

234
src/app/application.rs Normal file
View File

@ -0,0 +1,234 @@
pub use crate::app::app_state::AppState;
pub use crate::config::Config;
pub use crate::renderer::Renderer;
use crate::themes::*;
use crate::ui::caret::{CaretPosition, MoveDirection};
use crate::ui::*;
use std::rc::Rc;
use std::thread::sleep;
use std::time::Duration;
use sdl2::event::Event;
use sdl2::hint;
use sdl2::keyboard::{Keycode, Mod};
use sdl2::mouse::*;
use sdl2::pixels::{Color, PixelFormatEnum};
use sdl2::rect::{Point, Rect};
use sdl2::render::Canvas;
use sdl2::rwops::RWops;
use sdl2::surface::Surface;
use sdl2::ttf::Sdl2TtfContext;
use sdl2::video::Window;
use sdl2::EventPump;
use sdl2::{Sdl, TimerSubsystem, VideoSubsystem};
pub type WindowCanvas = Canvas<Window>;
#[derive(PartialEq, Clone, Debug)]
pub enum UpdateResult {
NoOp,
Stop,
RefreshPositions,
MouseLeftClicked(Point),
MoveCaret(Rect, CaretPosition),
DeleteFront,
DeleteBack,
Input(String),
InsertNewLine,
MoveCaretLeft,
MoveCaretRight,
MoveCaretUp,
MoveCaretDown,
Scroll { x: i32, y: i32 },
}
pub enum Task {
OpenFile { file_path: String },
}
pub struct Application {
config: Rc<Config>,
clear_color: Color,
sdl_context: Sdl,
canvas: WindowCanvas,
video_subsystem: VideoSubsystem,
tasks: Vec<Task>,
}
impl Application {
pub fn new() -> Self {
let config = Rc::new(Config::new());
let sdl_context = sdl2::init().unwrap();
hint::set("SDL_GL_MULTISAMPLEBUFFERS", "1");
hint::set("SDL_GL_MULTISAMPLESAMPLES", "8");
hint::set("SDL_GL_ACCELERATED_VISUAL", "1");
hint::set("SDL_HINT_RENDER_SCALE_QUALITY", "2");
hint::set("SDL_HINT_VIDEO_ALLOW_SCREENSAVER", "1");
let video_subsystem = sdl_context.video().unwrap();
let mut window: Window = video_subsystem
.window("Rider", config.width(), config.height())
.position_centered()
.resizable()
.opengl()
.build()
.unwrap();
let icon_bytes = include_bytes!("../../assets/gear-64x64.bmp").clone();
let mut rw = RWops::from_bytes(&icon_bytes).unwrap();
let mut icon = Surface::load_bmp_rw(&mut rw).unwrap();
window.set_icon(&mut icon);
let canvas = window.into_canvas().accelerated().build().unwrap();
Self {
sdl_context,
video_subsystem,
canvas,
tasks: vec![],
clear_color: config.theme().background().into(),
config,
}
}
pub fn init(&mut self) {
self.clear();
}
pub fn run(&mut self) {
let mut timer: TimerSubsystem = self.sdl_context.timer().unwrap();
let mut event_pump = self.sdl_context.event_pump().unwrap();
let font_context = sdl2::ttf::init().unwrap();
let texture_creator = self.canvas.texture_creator();
let sleep_time = Duration::new(0, 1_000_000_000u32 / 60);
let mut app_state = AppState::new(self.config.clone());
let mut renderer = Renderer::new(self.config.clone(), &font_context, &texture_creator);
app_state.prepare_ui(&mut renderer);
'running: loop {
match self.handle_events(&mut event_pump) {
UpdateResult::Stop => break 'running,
UpdateResult::RefreshPositions => (),
UpdateResult::NoOp => (),
UpdateResult::MoveCaret(_, _pos) => (),
UpdateResult::MouseLeftClicked(point) => {
app_state.on_left_click(&point, &mut self.video_subsystem);
}
UpdateResult::DeleteFront => {
app_state.file_editor_mut().delete_front(&mut renderer);
}
UpdateResult::DeleteBack => {
app_state.file_editor_mut().delete_back(&mut renderer);
}
UpdateResult::Input(text) => {
app_state.file_editor_mut().insert_text(text, &mut renderer);
}
UpdateResult::InsertNewLine => {
app_state.file_editor_mut().insert_new_line(&mut renderer);
}
UpdateResult::MoveCaretLeft => {
app_state.file_editor_mut().move_caret(MoveDirection::Left);
}
UpdateResult::MoveCaretRight => {
app_state.file_editor_mut().move_caret(MoveDirection::Right);
}
UpdateResult::MoveCaretUp => {
app_state.file_editor_mut().move_caret(MoveDirection::Up);
}
UpdateResult::MoveCaretDown => {
app_state.file_editor_mut().move_caret(MoveDirection::Down);
}
UpdateResult::Scroll { x, y } => {
app_state.file_editor_mut().scroll_to(x, y);
}
}
for task in self.tasks.iter() {
match task {
Task::OpenFile { file_path } => {
use crate::ui::file::editor_file::*;
app_state.open_file(file_path.clone(), &mut renderer);
}
}
}
self.tasks.clear();
self.clear();
app_state.update(timer.ticks() as i32, &UpdateContext::Nothing);
app_state.render(&mut self.canvas, &mut renderer, None);
self.present();
sleep(sleep_time);
}
}
pub fn open_file(&mut self, file_path: String) {
self.tasks.push(Task::OpenFile { file_path });
}
fn present(&mut self) {
self.canvas.present();
}
fn clear(&mut self) {
self.canvas.set_draw_color(self.clear_color.clone());
self.canvas.clear();
}
fn handle_events(&mut self, event_pump: &mut EventPump) -> UpdateResult {
for event in event_pump.poll_iter() {
match event {
Event::Quit { .. } => return UpdateResult::Stop,
Event::MouseButtonUp {
mouse_btn, x, y, ..
} => match mouse_btn {
MouseButton::Left => return UpdateResult::MouseLeftClicked(Point::new(x, y)),
_ => (),
},
Event::KeyDown { keycode, .. } => {
let keycode = if keycode.is_some() {
keycode.unwrap()
} else {
return UpdateResult::NoOp;
};
match keycode {
Keycode::Backspace => return UpdateResult::DeleteFront,
Keycode::Delete => return UpdateResult::DeleteBack,
Keycode::KpEnter | Keycode::Return => return UpdateResult::InsertNewLine,
Keycode::Left => return UpdateResult::MoveCaretLeft,
Keycode::Right => return UpdateResult::MoveCaretRight,
Keycode::Up => return UpdateResult::MoveCaretUp,
Keycode::Down => return UpdateResult::MoveCaretDown,
_ => UpdateResult::NoOp,
};
}
Event::TextInput { text, .. } => {
return UpdateResult::Input(text);
}
Event::MouseWheel {
direction, x, y, ..
} => {
match direction {
MouseWheelDirection::Normal => {
return UpdateResult::Scroll { x, y };
}
MouseWheelDirection::Flipped => {
return UpdateResult::Scroll { x, y: -y };
}
_ => {
// ignore
}
};
}
_ => (),
}
}
UpdateResult::NoOp
}
pub fn config(&self) -> &Rc<Config> {
&self.config
}
}

View File

@ -1,54 +1,64 @@
use crate::app::AppState;
use crate::ui::file::editor_file::EditorFile;
use crate::ui::text_character::TextCharacter;
use crate::ui::*;
use sdl2::rect::{Point, Rect};
pub fn move_caret_right(app_state: &mut AppState) {
let file: &EditorFile = match app_state.current_file() {
pub fn move_caret_right(file_editor: &mut FileEditor) {
let file: &EditorFile = match file_editor.file() {
None => return,
Some(f) => f,
};
let line = match file.get_character_at(app_state.caret().text_position()) {
Some(t) if t.is_new_line() => app_state.caret().line_number().clone() + 1,
Some(_) => app_state.caret().line_number().clone(),
None => 0,
};
let characters: Vec<&TextCharacter> = match file.get_line(&line) {
None => return,
Some(characters) => characters,
};
let mut idx = 0;
for (i, c) in characters.iter().enumerate() {
if c.position() == app_state.caret().text_position() {
idx = i + 1;
break;
}
}
let text_character: &TextCharacter = match characters.get(idx) {
let c: TextCharacter = match file.get_character_at(file_editor.caret().text_position() + 1) {
Some(text_character) => text_character,
None => return,
None => return, // EOF
};
let line = line - app_state.caret().line_number();
let pos = app_state.caret().position().moved(1, line as i32, 0);
let mut d: Rect = text_character.dest().clone();
if text_character.is_new_line() && idx > 0 {
let prev = match characters.get(idx - 1) {
Some(c) => c,
_ => return,
let caret_rect = file_editor.caret().dest().clone();
let pos = file_editor.caret().position();
let (d, p): (Rect, CaretPosition) = match (
c.is_last_in_line(),
c.is_new_line(),
c.dest().y() == caret_rect.y(),
) {
(true, true, false) => {
let prev: TextCharacter = if c.position() != 0 {
file.get_character_at(c.position() - 1).unwrap_or(c.clone())
} else {
c.clone()
};
d = prev.dest().clone();
d.set_x(d.x() + d.width() as i32);
let mut dest = prev.dest().clone();
dest.set_x(dest.x() + dest.width() as i32);
(dest, pos.moved(1, 0, 0))
}
app_state
(false, true, false) => {
let prev: TextCharacter = if c.position() != 0 {
file.get_character_at(c.position() - 1).unwrap_or(c.clone())
} else {
c.clone()
};
let mut dest = prev.dest().clone();
if !prev.is_new_line() {
dest.set_x(dest.x() + dest.width() as i32);
}
(dest, pos.moved(1, 0, 0))
}
(true, false, false) => {
// move after character, stay on current line
(c.dest().clone(), pos.moved(1, 0, 0))
}
(true, false, true) => {
// move to new line
(c.dest().clone(), pos.moved(1, 0, 0))
}
_ => (c.dest().clone(), pos.moved(1, 0, 0)),
};
file_editor
.caret_mut()
.move_caret(pos, Point::new(d.x(), d.y()));
.move_caret(p, Point::new(d.x(), d.y()));
}
pub fn move_caret_left(app_state: &mut AppState) {
let _file: &EditorFile = match app_state.current_file() {
pub fn move_caret_left(file_editor: &mut FileEditor) {
let _file: &EditorFile = match file_editor.file() {
None => return,
Some(f) => f,
};
let _line = app_state.caret().line_number();
let _line = file_editor.caret().line_number();
}

View File

@ -1,36 +1,22 @@
use crate::app::AppState;
use crate::app::*;
use crate::renderer::Renderer;
use crate::ui::caret::Caret;
use crate::ui::caret::CaretPosition;
use crate::ui::file::editor_file::EditorFile;
use crate::ui::get_text_character_rect;
use crate::ui::text_character::TextCharacter;
use sdl2::rect::Point;
use sdl2::rect::Rect;
use crate::ui::*;
use sdl2::rect::*;
fn get_character_at(app_state: &mut AppState, index: usize) -> Option<&TextCharacter> {
match app_state.current_file() {
None => return None,
Some(f) => f,
}
.get_character_at(index)
fn current_file_path(file_editor: &mut FileEditor) -> String {
file_editor
.file()
.map_or_else(|| String::new(), |f| f.path())
}
fn current_file_path(app_state: &mut AppState) -> String {
match app_state.current_file() {
Some(f) => f.path(),
_ => String::new(),
}
}
pub fn delete_front(app_state: &mut AppState) {
let mut buffer: String = if let Some(file) = app_state.current_file() {
pub fn delete_front(file_editor: &mut FileEditor, renderer: &mut Renderer) {
let mut buffer: String = if let Some(file) = file_editor.file() {
file
} else {
return;
}
.buffer();
let position: CaretPosition = app_state.caret().position().clone();
let position: CaretPosition = file_editor.caret().position().clone();
if position.text_position() == 0 {
return;
}
@ -44,100 +30,107 @@ pub fn delete_front(app_state: &mut AppState) {
_ if position.text_position() > 0 => position.moved(-1, 0, 0),
_ => position.moved(0, 0, 0),
};
let move_to = match get_character_at(app_state, position.text_position()) {
Some(character) => {
let move_to = file_editor
.file()
.and_then(|f| f.get_character_at(file_editor.caret().text_position()))
.and_then(|character| {
let dest: &Rect = character.dest();
Some((position, Point::new(dest.x(), dest.y())))
}
_ => None,
};
});
match move_to {
Some((position, point)) => app_state.caret_mut().move_caret(position, point),
None => app_state.caret_mut().reset_caret(),
Some((position, point)) => file_editor.caret_mut().move_caret(position, point),
None => file_editor.caret_mut().reset_caret(),
};
let new_file = EditorFile::new(
current_file_path(app_state),
let mut new_file = EditorFile::new(
current_file_path(file_editor),
buffer,
app_state.config().clone(),
file_editor.config().clone(),
);
app_state.replace_current_file(new_file);
new_file.prepare_ui(renderer);
file_editor.replace_current_file(new_file);
}
pub fn delete_back(app_state: &mut AppState) {
let file: &EditorFile = if let Some(file) = app_state.current_file() {
pub fn delete_back(file_editor: &mut FileEditor, renderer: &mut Renderer) {
let file: &EditorFile = if let Some(file) = file_editor.file() {
file
} else {
return;
};
let mut buffer: String = file.buffer();
let position: usize = app_state.caret().text_position();
let position: usize = file_editor.caret().text_position();
if position >= buffer.len() {
return;
}
buffer.remove(position);
let new_file = EditorFile::new(file.path(), buffer, app_state.config().clone());
app_state.replace_current_file(new_file);
let mut new_file = EditorFile::new(file.path(), buffer, file_editor.config().clone());
new_file.prepare_ui(renderer);
file_editor.replace_current_file(new_file);
}
pub fn insert_text(app_state: &mut AppState, text: String, renderer: &mut Renderer) {
let mut buffer: String = if let Some(file) = app_state.current_file() {
file
} else {
pub fn insert_text(file_editor: &mut FileEditor, text: String, renderer: &mut Renderer) {
let mut buffer: String = file_editor.file().map_or(String::new(), |f| f.buffer());
if buffer.is_empty() {
return;
}
.buffer();
let current = match get_character_at(app_state, app_state.caret().text_position()) {
let current = match file_editor
.file()
.and_then(|file| file.get_character_at(file_editor.caret().text_position()))
{
Some(c) => c,
_ => return,
};
let mut pos = Point::new(current.dest().x(), current.dest().y());
let mut position: CaretPosition = app_state.caret().position().clone();
let mut position: CaretPosition = file_editor.caret().position().clone();
for character in text.chars() {
buffer.insert(position.text_position(), character);
if let Some(rect) = get_text_character_rect(character, renderer) {
pos = pos + Point::new(rect.width() as i32, 0);
position = position.moved(1, 0, 0);
app_state.caret_mut().move_caret(position, pos.clone());
file_editor.caret_mut().move_caret(position, pos.clone());
}
}
let new_file = EditorFile::new(
current_file_path(app_state),
let mut new_file = EditorFile::new(
file_editor.file().map_or(String::new(), |f| f.path()),
buffer,
app_state.config().clone(),
file_editor.config().clone(),
);
app_state.replace_current_file(new_file);
new_file.prepare_ui(renderer);
file_editor.replace_current_file(new_file);
}
pub fn insert_new_line(app_state: &mut AppState, renderer: &mut Renderer) {
let mut buffer: String = if let Some(file) = app_state.current_file() {
pub fn insert_new_line(file_editor: &mut FileEditor, renderer: &mut Renderer) {
let mut buffer: String = if let Some(file) = file_editor.file() {
file
} else {
return;
}
.buffer();
let current = match get_character_at(app_state, app_state.caret().text_position()) {
let current = match file_editor
.file()
.and_then(|file| file.get_character_at(file_editor.caret().text_position()))
{
Some(c) => c,
_ => return,
};
let mut pos = Point::new(current.dest().x(), current.dest().y());
let mut position: CaretPosition = app_state.caret().position().clone();
let mut position: CaretPosition = file_editor.caret().position().clone();
buffer.insert(position.text_position(), '\n');
if let Some(rect) = get_text_character_rect('\n', renderer) {
pos = Point::new(
app_state.config().editor_left_margin(),
file_editor.config().editor_left_margin(),
pos.y() + rect.height() as i32,
);
position = position.moved(0, 1, 0);
app_state.caret_mut().move_caret(position, pos.clone());
file_editor.caret_mut().move_caret(position, pos.clone());
}
let new_file = EditorFile::new(
current_file_path(app_state),
let mut new_file = EditorFile::new(
current_file_path(file_editor),
buffer,
app_state.config().clone(),
file_editor.config().clone(),
);
app_state.replace_current_file(new_file);
new_file.prepare_ui(renderer);
file_editor.replace_current_file(new_file);
}

View File

@ -1,218 +1,9 @@
pub use crate::app::app_state::AppState;
pub use crate::config::Config;
pub use crate::renderer::Renderer;
use crate::themes::*;
use crate::ui::caret::{CaretPosition, MoveDirection};
use crate::ui::*;
use std::rc::Rc;
use std::thread::sleep;
use std::time::Duration;
use sdl2::event::Event;
use sdl2::hint;
use sdl2::keyboard::{Keycode, Mod};
use sdl2::mouse::MouseButton;
use sdl2::pixels::{Color, PixelFormatEnum};
use sdl2::rect::{Point, Rect};
use sdl2::render::Canvas;
use sdl2::rwops::RWops;
use sdl2::surface::Surface;
use sdl2::ttf::Sdl2TtfContext;
use sdl2::video::Window;
use sdl2::EventPump;
use sdl2::{Sdl, TimerSubsystem, VideoSubsystem};
pub mod app_state;
pub mod application;
pub mod caret_manager;
pub mod file_content_manager;
pub type WindowCanvas = Canvas<Window>;
#[derive(PartialEq, Clone, Debug)]
pub enum UpdateResult {
NoOp,
Stop,
RefreshPositions,
MouseLeftClicked(Point),
MoveCaret(Rect, CaretPosition),
DeleteFront,
DeleteBack,
Input(String),
InsertNewLine,
MoveCaretLeft,
MoveCaretRight,
MoveCaretUp,
MoveCaretDown,
}
pub enum Task {
OpenFile { file_path: String },
}
pub struct Application {
config: Rc<Config>,
clear_color: Color,
sdl_context: Sdl,
canvas: WindowCanvas,
video_subsystem: VideoSubsystem,
tasks: Vec<Task>,
}
impl Application {
pub fn new() -> Self {
let config = Rc::new(Config::new());
let sdl_context = sdl2::init().unwrap();
hint::set("SDL_GL_MULTISAMPLEBUFFERS", "1");
hint::set("SDL_GL_MULTISAMPLESAMPLES", "8");
hint::set("SDL_GL_ACCELERATED_VISUAL", "1");
hint::set("SDL_HINT_RENDER_SCALE_QUALITY", "2");
hint::set("SDL_HINT_VIDEO_ALLOW_SCREENSAVER", "1");
let video_subsystem = sdl_context.video().unwrap();
let mut window: Window = video_subsystem
.window("Rider", config.width(), config.height())
.position_centered()
.opengl()
.build()
.unwrap();
let icon_bytes = include_bytes!("../../assets/gear-64x64.bmp").clone();
let mut rw = RWops::from_bytes(&icon_bytes).unwrap();
let mut icon = Surface::load_bmp_rw(&mut rw).unwrap();
window.set_icon(&mut icon);
let canvas = window.into_canvas().accelerated().build().unwrap();
Self {
sdl_context,
video_subsystem,
canvas,
tasks: vec![],
clear_color: config.theme().background().into(),
config,
}
}
pub fn init(&mut self) {
self.clear();
}
pub fn run(&mut self) {
let mut timer: TimerSubsystem = self.sdl_context.timer().unwrap();
let mut event_pump = self.sdl_context.event_pump().unwrap();
let font_context = sdl2::ttf::init().unwrap();
let texture_creator = self.canvas.texture_creator();
let sleep_time = Duration::new(0, 1_000_000_000u32 / 60);
let mut app_state = AppState::new(self.config.clone());
let mut renderer = Renderer::new(self.config.clone(), &font_context, &texture_creator);
'running: loop {
match self.handle_events(&mut event_pump) {
UpdateResult::Stop => break 'running,
UpdateResult::RefreshPositions => (),
UpdateResult::NoOp => (),
UpdateResult::MoveCaret(_, _pos) => (),
UpdateResult::MouseLeftClicked(point) => {
app_state.on_left_click(&point, &mut self.video_subsystem);
}
UpdateResult::DeleteFront => {
app_state.delete_front();
}
UpdateResult::DeleteBack => {
app_state.delete_back();
}
UpdateResult::Input(text) => {
app_state.insert_text(text, &mut renderer);
}
UpdateResult::InsertNewLine => {
app_state.insert_new_line(&mut renderer);
}
UpdateResult::MoveCaretLeft => {
app_state.move_caret(MoveDirection::Left);
}
UpdateResult::MoveCaretRight => {
app_state.move_caret(MoveDirection::Right);
}
UpdateResult::MoveCaretUp => {
app_state.move_caret(MoveDirection::Up);
}
UpdateResult::MoveCaretDown => {
app_state.move_caret(MoveDirection::Down);
}
}
for task in self.tasks.iter() {
match task {
Task::OpenFile { file_path } => {
use crate::ui::file::editor_file::*;
app_state.open_file(file_path.clone());
}
}
}
self.tasks.clear();
self.clear();
app_state.update(timer.ticks() as i32);
app_state.render(&mut self.canvas, &mut renderer);
self.present();
sleep(sleep_time);
}
}
pub fn open_file(&mut self, file_path: String) {
self.tasks.push(Task::OpenFile { file_path });
}
fn present(&mut self) {
self.canvas.present();
}
fn clear(&mut self) {
self.canvas.set_draw_color(self.clear_color.clone());
self.canvas.clear();
}
fn handle_events(&mut self, event_pump: &mut EventPump) -> UpdateResult {
for event in event_pump.poll_iter() {
match event {
Event::Quit { .. } => return UpdateResult::Stop,
Event::MouseButtonUp {
mouse_btn, x, y, ..
} => match mouse_btn {
MouseButton::Left => return UpdateResult::MouseLeftClicked(Point::new(x, y)),
_ => (),
},
Event::KeyDown { keycode, .. } => {
let keycode = if keycode.is_some() {
keycode.unwrap()
} else {
return UpdateResult::NoOp;
};
match keycode {
Keycode::Backspace => return UpdateResult::DeleteFront,
Keycode::Delete => return UpdateResult::DeleteBack,
Keycode::KpEnter | Keycode::Return => return UpdateResult::InsertNewLine,
Keycode::Left => return UpdateResult::MoveCaretLeft,
Keycode::Right => return UpdateResult::MoveCaretRight,
Keycode::Up => return UpdateResult::MoveCaretUp,
Keycode::Down => return UpdateResult::MoveCaretDown,
_ => UpdateResult::NoOp,
};
}
Event::TextInput { text, .. } => {
println!("text input: {}", text);
return UpdateResult::Input(text);
}
_ => (),
}
}
UpdateResult::NoOp
}
pub fn config(&self) -> &Rc<Config> {
&self.config
}
}
pub use crate::app::app_state::*;
pub use crate::app::application::*;
pub use crate::app::caret_manager::*;
pub use crate::app::file_content_manager::*;

View File

@ -19,6 +19,11 @@ pub fn create() {
if !log_dir().exists() {
fs::create_dir_all(&log_dir()).unwrap_or_else(|_| panic!("Cannot create log directory"));
}
if !project_dir().exists() {
fs::create_dir_all(&project_dir())
.unwrap_or_else(|_| panic!("Cannot create project directory"));
}
}
fn write_default_fonts() {

View File

@ -26,3 +26,11 @@ pub fn config_dir() -> PathBuf {
config_dir.push("rider");
config_dir
}
pub fn project_dir() -> PathBuf {
let runtime = dirs::runtime_dir().unwrap();
let mut project_dir = runtime.clone();
project_dir.push(".rider");
project_dir
}

View File

@ -56,6 +56,7 @@ impl EditorConfig {
pub struct Config {
width: u32,
height: u32,
scroll_speed: i32,
menu_height: u16,
editor_config: EditorConfig,
theme: Theme,
@ -74,12 +75,18 @@ impl Config {
Self {
width: 1024,
height: 860,
scroll_speed: 10,
menu_height: 60,
theme: Theme::load(editor_config.current_theme().clone()),
editor_config,
extensions_mapping,
}
}
pub fn scroll_speed(&self) -> i32 {
self.scroll_speed
}
pub fn width(&self) -> u32 {
self.width
}

View File

@ -3,7 +3,7 @@ use std::ops::Deref;
pub mod plain;
pub mod rust_lang;
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
pub enum Language {
PlainText,
Rust,

View File

@ -7,7 +7,7 @@ pub mod lexer {
lexer! {
fn next_token(text: 'a) -> (TokenType, &'a str);
r"( +|\t+|\n+)" => (TokenType::Whitespace {
r"( +|\t+|\n)" => (TokenType::Whitespace {
token: Token::new(text.to_string(), 0, 0, 0, 0)
}, text),

View File

@ -10,16 +10,8 @@ pub struct CaretColor {
impl Default for CaretColor {
fn default() -> Self {
Self {
bright: ThemeConfig::new(
SerdeColor::new(0, 0, 0, 0),
false,
false,
),
blur: ThemeConfig::new(
SerdeColor::new(0, 0, 0, 0),
false,
false,
),
bright: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false),
blur: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false),
}
}
}

View File

@ -1,4 +1,4 @@
use crate::app::{UpdateResult, WindowCanvas};
use crate::app::{UpdateResult as UR, WindowCanvas as WC};
use crate::config::Config;
use crate::renderer::Renderer;
use crate::ui::text_character::TextCharacter;
@ -10,7 +10,7 @@ use std::ops::Deref;
use std::rc::Rc;
#[derive(Clone, Debug, PartialEq)]
enum CaretState {
pub enum CaretState {
Bright,
Blur,
}
@ -95,25 +95,6 @@ pub struct CaretRenderPosition {
reset_position: Rect,
}
impl CaretRenderPosition {
pub fn dest(&self) -> &Rect {
&self.dest
}
pub fn reset_position(&self) -> &Rect {
&self.reset_position
}
pub fn reset(&mut self) {
self.dest = self.reset_position.clone()
}
pub fn move_to(&mut self, p: &Point) {
self.dest.set_x(p.x());
self.dest.set_y(p.y());
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CaretColor {
bright: Color,
@ -140,7 +121,7 @@ pub struct Caret {
blink_delay: u8,
state: CaretState,
position: CaretPosition,
render_position: CaretRenderPosition,
dest: Rect,
colors: CaretColor,
}
@ -151,20 +132,7 @@ impl Caret {
Self {
state: CaretState::Bright,
blink_delay: 0,
render_position: CaretRenderPosition {
dest: Rect::new(
config.editor_left_margin(),
config.editor_top_margin(),
4,
0,
),
reset_position: Rect::new(
config.editor_left_margin(),
config.editor_top_margin(),
4,
0,
),
},
dest: Rect::new(0, 0, 6, 0),
colors: CaretColor { bright, blur },
pending: true,
position: CaretPosition {
@ -172,7 +140,6 @@ impl Caret {
line_number: 0,
line_position: 0,
},
// config,
}
}
@ -185,13 +152,19 @@ impl Caret {
}
pub fn reset_caret(&mut self) {
self.render_position.reset();
self.dest.set_x(0);
self.dest.set_y(0);
self.position.reset();
}
pub fn move_caret(&mut self, position: CaretPosition, pos: Point) {
self.position = position;
self.render_position.move_to(&pos);
self.dest.set_x(pos.x());
self.dest.set_y(pos.y());
}
pub fn dest(&self) -> &Rect {
&self.dest
}
pub fn position(&self) -> &CaretPosition {
@ -208,52 +181,66 @@ impl Deref for Caret {
}
impl Render for Caret {
fn render(&mut self, canvas: &mut WindowCanvas, renderer: &mut Renderer) -> UpdateResult {
if self.pending {
if let Some(rect) = get_text_character_rect('W', renderer) {
let mut dest = self.render_position.dest().clone();
dest.set_height(rect.height());
let reset_position = dest.clone();
self.render_position = CaretRenderPosition {
dest,
reset_position,
fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, parent: Parent) -> UR {
let dest = match parent {
Some(parent) => move_render_point(parent.render_start_point(), self.dest()),
None => self.dest().clone(),
};
}
self.pending = false;
}
let dest = self.render_position.dest();
let start = Point::new(dest.x(), dest.y());
let end = Point::new(dest.x(), dest.y() + dest.height() as i32);
let color = match self.state {
CaretState::Bright => self.colors.bright(),
CaretState::Blur => self.colors.blur(),
};
canvas.set_draw_color(color.clone());
}
.clone();
canvas.set_draw_color(color);
canvas
.draw_line(start, end)
.unwrap_or_else(|_| panic!("Failed to draw a caret"));
UpdateResult::NoOp
UR::NoOp
}
fn prepare_ui(&mut self, renderer: &mut Renderer) {
if !self.pending {
return;
}
if let Some(rect) = get_text_character_rect('W', renderer) {
self.dest.set_height(rect.height());
}
self.pending = false;
}
}
impl Update for Caret {
fn update(&mut self, _ticks: i32) -> UpdateResult {
fn update(&mut self, _ticks: i32, _context: &UpdateContext) -> UR {
self.blink_delay += 1;
if self.blink_delay >= 30 {
self.blink_delay = 0;
self.toggle_state();
}
UpdateResult::NoOp
UR::NoOp
}
}
impl ClickHandler for Caret {
fn on_left_click(&mut self, _point: &Point) -> UpdateResult {
// self.move_caret(Point::new(self.position.x(), self.position.y()));
UpdateResult::NoOp
fn on_left_click(&mut self, _point: &Point, _context: &UpdateContext) -> UR {
UR::NoOp
}
fn is_left_click_target(&self, point: &Point) -> bool {
is_in_rect(point, &self.render_position.dest())
fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool {
is_in_rect(
point,
&match context {
&UpdateContext::ParentPosition(p) => move_render_point(p, self.dest()),
_ => self.dest().clone(),
},
)
}
}
impl RenderBox for Caret {
fn render_start_point(&self) -> Point {
self.dest().top_left()
}
}

View File

@ -1,31 +1,31 @@
use sdl2::rect::{Point, Rect};
use std::rc::Rc;
use std::sync::*;
use crate::app::{UpdateResult, WindowCanvas};
use crate::app::{UpdateResult as UR, WindowCanvas as WC};
use crate::config::Config;
use crate::renderer::Renderer;
use crate::ui::file::editor_file_section::EditorFileSection;
use crate::ui::text_character::TextCharacter;
use crate::ui::*;
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct EditorFile {
path: String,
sections: Vec<EditorFileSection>,
render_position: Rect,
buffer: String,
config: Rc<Config>,
line_height: u32,
}
impl EditorFile {
pub fn new(path: String, buffer: String, config: Rc<Config>) -> Self {
use std::path::Path;
let p = Path::new(&path);
let ext = match p.extension() {
Some(s) => s.to_str().unwrap_or("txt"),
None => "txt",
}
let ext = Path::new(&path)
.extension()
.and_then(|p| p.to_str())
.map_or("txt", |s| s)
.to_string();
let sections = vec![EditorFileSection::new(buffer.clone(), ext, config.clone())];
let x = config.editor_left_margin();
@ -37,6 +37,7 @@ impl EditorFile {
render_position: Rect::new(x, y, 0, 0),
buffer,
config,
line_height: 0,
}
}
@ -48,10 +49,23 @@ impl EditorFile {
self.path.clone()
}
pub fn get_character_at(&self, index: usize) -> Option<&TextCharacter> {
pub fn sections(&self) -> &Vec<EditorFileSection> {
&self.sections
}
pub fn line_height(&self) -> u32 {
self.line_height
}
pub fn render_position(&self) -> &Rect {
&self.render_position
}
pub fn get_character_at(&self, index: usize) -> Option<TextCharacter> {
for section in self.sections.iter() {
if let Some(text_character) = section.get_character_at(index) {
return Some(text_character);
let character = section.get_character_at(index);
if character.is_some() {
return character;
}
}
None
@ -59,7 +73,6 @@ impl EditorFile {
pub fn get_line(&self, line: &usize) -> Option<Vec<&TextCharacter>> {
let mut vec: Vec<&TextCharacter> = vec![];
for section in self.sections.iter() {
match section.get_line(line) {
Some(v) => vec.append(&mut v.clone()),
@ -74,8 +87,23 @@ impl EditorFile {
}
}
pub fn get_last_at_line(&self, line: usize) -> Option<TextCharacter> {
let mut current = None;
for section in self.sections.iter() {
let c = section.get_last_at_line(line);
if c.is_some() {
current = c;
}
}
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 = self.render_position.clone();
let mut current: Rect = Rect::new(0, 0, 0, 0);
for section in self.sections.iter_mut() {
section.update_positions(&mut current);
}
@ -83,47 +111,66 @@ impl EditorFile {
}
impl Render for EditorFile {
fn render(&mut self, canvas: &mut WindowCanvas, renderer: &mut Renderer) -> UpdateResult {
let mut res = UpdateResult::NoOp;
for section in self.sections.iter_mut() {
res = section.render(canvas, renderer);
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) -> UR {
for section in self.sections.iter() {
section.render(canvas, renderer, parent);
}
UR::NoOp
}
fn prepare_ui(&mut self, renderer: &mut Renderer) {
for section in self.sections.iter_mut() {
section.prepare_ui(renderer);
}
if let Some(r) = get_text_character_rect('W', renderer) {
self.line_height = r.height();
}
if res == UpdateResult::RefreshPositions {
self.refresh_characters_position();
for section in self.sections.iter_mut() {
section.render(canvas, renderer);
}
}
UpdateResult::NoOp
}
}
impl Update for EditorFile {
fn update(&mut self, ticks: i32) -> UpdateResult {
let mut result = UpdateResult::NoOp;
fn update(&mut self, ticks: i32, context: &UpdateContext) -> UR {
let mut result = UR::NoOp;
for section in self.sections.iter_mut() {
result = section.update(ticks);
result = section.update(ticks, context);
}
result
}
}
impl ClickHandler for EditorFile {
fn on_left_click(&mut self, point: &Point) -> UpdateResult {
for section in self.sections.iter_mut() {
if section.is_left_click_target(point) {
return section.on_left_click(point);
fn on_left_click(&mut self, point: &Point, context: &UpdateContext) -> UR {
let mut index = -1;
for (i, section) in self.sections.iter().enumerate() {
if section.is_left_click_target(point, context) {
index = i as i32;
break;
}
}
UpdateResult::NoOp
if index >= 0 {
let context = UpdateContext::ParentPosition(self.render_start_point());
return self
.get_section_at_mut(index as usize)
.unwrap()
.on_left_click(point, &context);
}
UR::NoOp
}
fn is_left_click_target(&self, point: &Point) -> 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() {
if section.is_left_click_target(point) {
if section.is_left_click_target(point, &context) {
return true;
}
}
false
}
}
impl RenderBox for EditorFile {
fn render_start_point(&self) -> Point {
self.render_position.top_left()
}
}

View File

@ -1,7 +1,8 @@
use sdl2::rect::{Point, Rect};
use std::cell::Cell;
use std::rc::Rc;
use crate::app::{UpdateResult, WindowCanvas};
use crate::app::{UpdateResult as UR, WindowCanvas as WC};
use crate::config::Config;
use crate::lexer::Language;
use crate::renderer::Renderer;
@ -9,7 +10,7 @@ use crate::ui::file::editor_file_token::EditorFileToken;
use crate::ui::text_character::TextCharacter;
use crate::ui::*;
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct EditorFileSection {
tokens: Vec<EditorFileToken>,
language: Language,
@ -27,9 +28,19 @@ impl EditorFileSection {
let lexer_tokens = lexer::parse(buffer.clone(), language);
let mut tokens: Vec<EditorFileToken> = vec![];
for token_type in lexer_tokens {
let token = EditorFileToken::new(token_type, config.clone());
tokens.push(token.clone());
let mut iterator = lexer_tokens.iter().peekable();
loop {
let token_type = match iterator.next() {
Some(t) => t,
_ => break,
};
let next = iterator.peek();
let token = EditorFileToken::new(
token_type,
next.map_or(true, |t| t.is_new_line()),
config.clone(),
);
tokens.push(token);
}
let language = Language::PlainText;
Self {
@ -45,10 +56,11 @@ impl EditorFileSection {
}
}
pub fn get_character_at(&self, index: usize) -> Option<&TextCharacter> {
pub fn get_character_at(&self, index: usize) -> Option<TextCharacter> {
for token in self.tokens.iter() {
if let Some(text_character) = token.get_character_at(index) {
return Some(text_character);
let character = token.get_character_at(index);
if character.is_some() {
return character;
}
}
None
@ -68,47 +80,73 @@ impl EditorFileSection {
Some(vec)
}
}
pub fn get_last_at_line(&self, line: usize) -> Option<TextCharacter> {
let mut current: Option<TextCharacter> = None;
for token in self.tokens.iter() {
if !token.is_last_in_line() {
continue;
}
let c = token.get_last_at_line(line);
if c.is_some() {
current = c;
}
}
current
}
}
impl Render for EditorFileSection {
fn render(&mut self, canvas: &mut WindowCanvas, renderer: &mut Renderer) -> UpdateResult {
let mut res = UpdateResult::NoOp;
for character in self.tokens.iter_mut() {
let r = character.render(canvas, renderer);
if res == UpdateResult::NoOp {
res = r;
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) -> UR {
for token in self.tokens.iter() {
token.render(canvas, renderer, parent);
}
UR::NoOp
}
fn prepare_ui(&mut self, renderer: &mut Renderer) {
for token in self.tokens.iter_mut() {
token.prepare_ui(renderer);
}
res
}
}
impl Update for EditorFileSection {
fn update(&mut self, ticks: i32) -> UpdateResult {
let mut result = UpdateResult::NoOp;
for file_char in self.tokens.iter_mut() {
result = file_char.update(ticks)
fn update(&mut self, ticks: i32, context: &UpdateContext) -> UR {
let mut result = UR::NoOp;
for token in self.tokens.iter_mut() {
result = token.update(ticks, context)
}
result
}
}
impl ClickHandler for EditorFileSection {
fn on_left_click(&mut self, point: &Point) -> UpdateResult {
fn on_left_click(&mut self, point: &Point, context: &UpdateContext) -> UR {
for token in self.tokens.iter_mut() {
if token.is_left_click_target(point) {
return token.on_left_click(point);
if token.is_left_click_target(point, context) {
return token.on_left_click(point, context);
}
}
UpdateResult::NoOp
UR::NoOp
}
fn is_left_click_target(&self, point: &Point) -> bool {
for token in self.tokens.iter() {
if token.is_left_click_target(point) {
fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool {
let mut i = 0;
loop {
if i == self.tokens.len() {
break;
}
match self.tokens.get(i) {
Some(token) => {
if token.is_left_click_target(point, context) {
return true;
}
}
None => break,
}
i += 1;
}
false
}
}

View File

@ -1,9 +1,8 @@
use crate::app::{UpdateResult, WindowCanvas};
use crate::app::{UpdateResult as UR, WindowCanvas as WC};
use crate::config::Config;
use crate::lexer::TokenType;
use crate::renderer::managers::{FontDetails, TextDetails};
use crate::renderer::Renderer;
use crate::ui::text_character::*;
use crate::ui::*;
use sdl2::pixels::Color;
use sdl2::rect::{Point, Rect};
@ -11,32 +10,55 @@ use sdl2::render::Texture;
use sdl2::ttf::Font;
use std::rc::Rc;
#[derive(Clone)]
impl TokenType {
pub fn to_color(&self, config: &Rc<Config>) -> Color {
let config = config.theme().code_highlighting();
match self {
&TokenType::Whitespace { .. } => config.whitespace().color().into(),
&TokenType::Keyword { .. } => config.keyword().color().into(),
&TokenType::String { .. } => config.string().color().into(),
&TokenType::Number { .. } => config.number().color().into(),
&TokenType::Identifier { .. } => config.identifier().color().into(),
&TokenType::Literal { .. } => config.literal().color().into(),
&TokenType::Comment { .. } => config.comment().color().into(),
&TokenType::Operator { .. } => config.operator().color().into(),
&TokenType::Separator { .. } => config.separator().color().into(),
}
}
}
#[derive(Clone, Debug)]
pub struct EditorFileToken {
last_in_line: bool,
characters: Vec<TextCharacter>,
token_type: TokenType,
token_type: Rc<TokenType>,
config: Rc<Config>,
}
impl EditorFileToken {
pub fn new(token_type: TokenType, config: Rc<Config>) -> Self {
pub fn new(token_type: &TokenType, last_in_line: bool, config: Rc<Config>) -> Self {
Self {
last_in_line,
characters: vec![],
token_type,
token_type: Rc::new(token_type.clone()),
config,
}
}
pub fn is_last_in_line(&self) -> bool {
self.last_in_line
}
pub fn update_position(&mut self, current: &mut Rect) {
for text_character in self.characters.iter_mut() {
text_character.update_position(current);
}
}
pub fn get_character_at(&self, index: usize) -> Option<&TextCharacter> {
pub fn get_character_at(&self, index: usize) -> Option<TextCharacter> {
for character in self.characters.iter() {
if character.position() == index {
return Some(&character);
return Some(character.clone());
}
}
None
@ -70,32 +92,17 @@ impl EditorFileToken {
}
}
fn update_view(&mut self, renderer: &mut Renderer) -> UpdateResult {
let config = renderer.config().theme().code_highlighting();
let color: Color = match self.token_type {
TokenType::Whitespace { .. } => config.whitespace().color().into(),
TokenType::Keyword { .. } => config.keyword().color().into(),
TokenType::String { .. } => config.string().color().into(),
TokenType::Number { .. } => config.number().color().into(),
TokenType::Identifier { .. } => config.identifier().color().into(),
TokenType::Literal { .. } => config.literal().color().into(),
TokenType::Comment { .. } => config.comment().color().into(),
TokenType::Operator { .. } => config.operator().color().into(),
TokenType::Separator { .. } => config.separator().color().into(),
};
for (index, c) in self.token_type.text().chars().enumerate() {
let mut text_character = TextCharacter::new(
c.clone(),
self.token_type.start() + index,
self.token_type.line(),
color,
self.config.clone(),
);
text_character.update_view(renderer);
self.characters.push(text_character);
pub fn get_last_at_line(&self, line: usize) -> Option<TextCharacter> {
let mut current: Option<&TextCharacter> = None;
for text_character in self.characters.iter() {
if !text_character.is_last_in_line() {
continue;
}
UpdateResult::RefreshPositions
if text_character.line() == line {
current = Some(text_character);
}
}
current.map(|c| c.clone())
}
}
@ -104,42 +111,60 @@ impl Render for EditorFileToken {
* Must first create targets so even if new line appear renderer will know
* where move render starting point
*/
fn render(&mut self, canvas: &mut WindowCanvas, renderer: &mut Renderer) -> UpdateResult {
if self.characters.is_empty() {
return self.update_view(renderer);
}
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) -> UR {
if self.token_type.is_new_line() {
return UpdateResult::NoOp;
return UR::NoOp;
}
for text_character in self.characters.iter_mut() {
text_character.render(canvas, renderer);
for text_character in self.characters.iter() {
text_character.render(canvas, renderer, parent);
}
UR::NoOp
}
fn prepare_ui(&mut self, renderer: &mut Renderer) {
if !self.characters.is_empty() {
return;
}
let color: Color = self.token_type.to_color(renderer.config());
let chars: Vec<char> = self.token_type.text().chars().collect();
for (index, c) in chars.iter().enumerate() {
let last_in_line = self.last_in_line && index + 1 == chars.len();
let mut text_character: TextCharacter = TextCharacter::new(
c.clone(),
self.token_type.start() + index,
self.token_type.line(),
last_in_line,
color,
self.config.clone(),
);
text_character.prepare_ui(renderer);
self.characters.push(text_character);
}
UpdateResult::NoOp
}
}
impl Update for EditorFileToken {
fn update(&mut self, ticks: i32) -> UpdateResult {
fn update(&mut self, ticks: i32, context: &UpdateContext) -> UR {
for text_character in self.characters.iter_mut() {
text_character.update(ticks);
text_character.update(ticks, context);
}
UpdateResult::NoOp
UR::NoOp
}
}
impl ClickHandler for EditorFileToken {
fn on_left_click(&mut self, point: &Point) -> UpdateResult {
fn on_left_click(&mut self, point: &Point, context: &UpdateContext) -> UR {
for text_character in self.characters.iter_mut() {
if text_character.is_left_click_target(point) {
return text_character.on_left_click(point);
if text_character.is_left_click_target(point, context) {
return text_character.on_left_click(point, context);
}
}
UpdateResult::NoOp
UR::NoOp
}
fn is_left_click_target(&self, point: &Point) -> bool {
fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool {
for text_character in self.characters.iter() {
if text_character.is_left_click_target(point) {
if text_character.is_left_click_target(point, context) {
return true;
}
}

View File

@ -1,3 +1,7 @@
pub mod editor_file;
pub mod editor_file_section;
pub mod editor_file_token;
pub use crate::ui::file::editor_file::*;
pub use crate::ui::file::editor_file_section::*;
pub use crate::ui::file::editor_file_token::*;

270
src/ui/file_editor.rs Normal file
View File

@ -0,0 +1,270 @@
use sdl2::rect::*;
use std::borrow::*;
use std::mem;
use std::rc::Rc;
use crate::app::*;
use crate::app::{UpdateResult as UR, WindowCanvas as WS};
use crate::ui::*;
pub struct FileEditor {
dest: Rect,
scroll: Point,
caret: Caret,
file: Option<EditorFile>,
config: Rc<Config>,
}
impl FileEditor {
pub fn new(dest: Rect, config: Rc<Config>) -> Self {
Self {
dest,
scroll: Point::new(0, 0),
caret: Caret::new(config.clone()),
file: None,
config,
}
}
pub fn config(&self) -> &Rc<Config> {
&self.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) {
self.scroll = self.scroll
+ Point::new(
self.config.scroll_speed() * x,
self.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) -> UR {
match self.file() {
Some(file) => file.render(canvas, renderer, Some(self)),
_ => UR::NoOp,
};
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 {
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
}
}
#[cfg(test)]
mod tests {
use crate::app::*;
use crate::ui::*;
use sdl2::rect::*;
use sdl2::*;
use std::borrow::*;
use std::rc::*;
#[test]
fn replace_file() {
let config = Rc::new(Config::new());
let mut editor = FileEditor::new(Rect::new(0, 0, 100, 100), config.clone());
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());
}
#[test]
fn add_text() {
let config = Rc::new(Config::new());
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
let window = video_subsystem
.window("Test", 1, 1)
.borderless()
.opengl()
.build()
.unwrap();
let canvas = window.into_canvas().accelerated().build().unwrap();
let font_context = sdl2::ttf::init().unwrap();
let texture_creator = canvas.texture_creator();
let mut renderer = Renderer::new(config.clone(), &font_context, &texture_creator);
let mut editor = FileEditor::new(Rect::new(0, 0, 100, 100), config.clone());
let mut file = EditorFile::new("./foo.txt".to_string(), "foo".to_string(), config.clone());
file.prepare_ui(&mut renderer);
assert_eq!(editor.open_file(file).is_none(), true);
assert_eq!(editor.caret().position().text_position(), 0);
assert_eq!(editor.file().is_some(), true);
assert_eq!(editor.file().unwrap().sections().len(), 1);
assert_eq!(editor.file().unwrap().get_character_at(0).is_some(), true);
editor.insert_text("z".to_string(), &mut renderer);
assert_eq!(editor.caret().position().text_position(), 1);
assert_eq!(editor.file().is_some(), true);
assert_eq!(editor.file().unwrap().buffer(), "zfoo".to_string());
}
}

View File

@ -7,17 +7,21 @@ use sdl2::rect::{Point, Rect};
use std::rc::Rc;
pub struct MenuBar {
border_color: Color,
background_color: Color,
dest: Rect,
config: Rc<Config>,
pending: bool,
}
impl MenuBar {
pub fn new(config: Rc<Config>) -> Self {
Self {
background_color: Color::RGB(10, 10, 10),
border_color: Color::RGB(10, 10, 10),
background_color: config.theme().background().into(),
dest: Rect::new(0, 0, 0, 0),
config,
pending: true,
}
}
@ -31,28 +35,66 @@ impl MenuBar {
}
impl Render for MenuBar {
fn render(&mut self, canvas: &mut WindowCanvas, _renderer: &mut Renderer) -> UpdateResult {
fn render(
&self,
canvas: &mut WindowCanvas,
_renderer: &mut Renderer,
parent: Parent,
) -> UpdateResult {
canvas.set_draw_color(self.background_color.clone());
canvas
.fill_rect(match parent {
None => self.dest.clone(),
Some(parent) => move_render_point(parent.render_start_point(), self.dest()),
})
.unwrap_or_else(|_| panic!("Failed to draw main menu background"));
canvas.set_draw_color(self.border_color.clone());
canvas
.draw_rect(match parent {
None => self.dest.clone(),
Some(parent) => move_render_point(parent.render_start_point(), self.dest()),
})
.unwrap_or_else(|_| panic!("Failed to draw main menu background"));
UpdateResult::NoOp
}
fn prepare_ui(&mut self, _renderer: &mut Renderer) {
if !self.pending {
return;
}
let width = self.config.width();
let height = self.config.menu_height() as u32;
self.dest = Rect::new(0, 0, width, height);
canvas.set_draw_color(self.background_color.clone());
canvas.draw_rect(self.dest.clone()).unwrap();
UpdateResult::NoOp
self.pending = false;
}
}
impl Update for MenuBar {
fn update(&mut self, _ticks: i32) -> UpdateResult {
fn update(&mut self, _ticks: i32, _context: &UpdateContext) -> UpdateResult {
UpdateResult::NoOp
}
}
impl ClickHandler for MenuBar {
fn on_left_click(&mut self, _point: &Point) -> UpdateResult {
fn on_left_click(&mut self, _point: &Point, _context: &UpdateContext) -> UpdateResult {
unimplemented!()
}
fn is_left_click_target(&self, point: &Point) -> bool {
is_in_rect(point, self.dest())
fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool {
is_in_rect(
point,
&match context {
&UpdateContext::ParentPosition(p) => move_render_point(p, self.dest()),
_ => self.dest().clone(),
},
)
}
}
impl RenderBox for MenuBar {
fn render_start_point(&self) -> Point {
self.dest.top_left()
}
}

View File

@ -7,11 +7,30 @@ use crate::renderer::Renderer;
pub mod caret;
pub mod file;
pub mod file_editor;
pub mod menu_bar;
pub mod project_tree;
pub mod text_character;
pub use crate::ui::caret::*;
pub use crate::ui::file::*;
pub use crate::ui::file_editor::*;
pub use crate::ui::menu_bar::*;
pub use crate::ui::project_tree::*;
pub use crate::ui::text_character::*;
pub type Parent<'l> = Option<&'l RenderBox>;
pub type ParentMut<'l> = Option<&'l mut RenderBox>;
pub enum UpdateContext<'l> {
Nothing,
ParentPosition(Point),
CurrentFile(&'l mut EditorFile),
}
#[inline]
pub fn is_in_rect(point: &Point, rect: &Rect) -> bool {
let start = Point::new(rect.x(), rect.y());
let start = rect.top_left();
let end = Point::new(
rect.x() + (rect.width() as i32),
rect.y() + (rect.height() as i32),
@ -35,16 +54,32 @@ pub fn get_text_character_rect(c: char, renderer: &mut Renderer) -> Option<Rect>
}
}
#[inline]
pub fn move_render_point(p: Point, d: &Rect) -> Rect {
Rect::new(d.x() + p.x(), d.y() + p.y(), d.width(), d.height())
}
pub trait Render {
fn render(&mut self, canvas: &mut WindowCanvas, renderer: &mut Renderer) -> UpdateResult;
fn render(
&self,
canvas: &mut WindowCanvas,
renderer: &mut Renderer,
parent: Parent,
) -> UpdateResult;
fn prepare_ui(&mut self, renderer: &mut Renderer);
}
pub trait Update {
fn update(&mut self, ticks: i32) -> UpdateResult;
fn update(&mut self, ticks: i32, context: &UpdateContext) -> UpdateResult;
}
pub trait ClickHandler {
fn on_left_click(&mut self, point: &Point) -> UpdateResult;
fn on_left_click(&mut self, point: &Point, context: &UpdateContext) -> UpdateResult;
fn is_left_click_target(&self, point: &Point) -> bool;
fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool;
}
pub trait RenderBox {
fn render_start_point(&self) -> Point;
}

View File

@ -0,0 +1 @@

View File

@ -1,4 +1,4 @@
use crate::app::{UpdateResult, WindowCanvas};
use crate::app::{UpdateResult as UR, WindowCanvas as WC};
use crate::config::Config;
use crate::lexer::TokenType;
use crate::renderer::managers::{FontDetails, TextDetails};
@ -12,12 +12,12 @@ use sdl2::render::Texture;
use sdl2::ttf::Font;
use std::rc::Rc;
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct TextCharacter {
pending: bool,
text_character: char,
position: usize,
line: usize,
last_in_line: bool,
source: Rect,
dest: Rect,
color: Color,
@ -29,14 +29,15 @@ impl TextCharacter {
text_character: char,
position: usize,
line: usize,
last_in_line: bool,
color: Color,
config: Rc<Config>,
) -> Self {
Self {
pending: true,
text_character,
position,
line,
last_in_line,
source: Rect::new(0, 0, 0, 0),
dest: Rect::new(0, 0, 0, 0),
color,
@ -44,6 +45,10 @@ impl TextCharacter {
}
}
pub fn is_last_in_line(&self) -> bool {
self.last_in_line
}
pub fn dest(&self) -> &Rect {
&self.dest
}
@ -59,8 +64,10 @@ impl TextCharacter {
pub fn update_position(&mut self, current: &mut Rect) {
if self.is_new_line() {
let y = self.source.height() as i32;
current.set_x(self.config.editor_left_margin());
current.set_x(0);
current.set_y(current.y() + y);
self.dest.set_x(current.x());
self.dest.set_y(current.y());
} else {
self.dest.set_x(current.x());
self.dest.set_y(current.y());
@ -70,7 +77,62 @@ impl TextCharacter {
}
}
pub fn update_view(&mut self, renderer: &mut Renderer) -> UpdateResult {
#[inline]
pub fn is_new_line(&self) -> bool {
self.text_character == '\n'
}
pub fn position(&self) -> usize {
self.position
}
pub fn line(&self) -> usize {
self.line
}
pub fn text_character(&self) -> char {
self.text_character.clone()
}
}
impl Render for TextCharacter {
/**
* Must first create targets so even if new line appear renderer will know
* where move render starting point
*/
fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: Parent) -> UR {
if self.is_new_line() {
return UR::NoOp;
}
let config = renderer.config().editor_config();
let font_details =
FontDetails::new(config.font_path().as_str(), config.character_size().clone());
let font = renderer
.font_manager()
.load(&font_details)
.unwrap_or_else(|_| panic!("Could not load font for {:?}", font_details));
let c = self.text_character.clone();
let mut details = TextDetails {
text: c.to_string(),
color: self.color.clone(),
font: font_details.clone(),
};
let dest = match parent {
None => self.dest.clone(),
Some(parent) => move_render_point(parent.render_start_point(), self.dest()),
};
if let Ok(texture) = renderer.texture_manager().load_text(&mut details, &font) {
renderer.render_texture(canvas, &texture, &self.source, &dest);
}
// let c = Color::RGB(255, 0, 0);
// canvas.set_draw_color(c);
// canvas.draw_rect(dest.clone()).unwrap();
UR::NoOp
}
fn prepare_ui(&mut self, renderer: &mut Renderer) {
let config = renderer.config().editor_config();
let font_details =
FontDetails::new(config.font_path().as_str(), config.character_size().clone());
@ -96,83 +158,36 @@ impl TextCharacter {
.texture_manager()
.load_text(&mut details, &font)
.unwrap_or_else(|_| panic!("Could not create texture for {:?}", self.text_character));
self.pending = false;
UpdateResult::RefreshPositions
}
#[inline]
pub fn is_new_line(&self) -> bool {
self.text_character == '\n'
}
#[inline]
pub fn is_pending(&self) -> bool {
self.pending
}
pub fn position(&self) -> usize {
self.position
}
pub fn line(&self) -> usize {
self.line
}
pub fn text_character(&self) -> char {
self.text_character.clone()
}
}
impl Render for TextCharacter {
/**
* Must first create targets so even if new line appear renderer will know
* where move render starting point
*/
fn render(&mut self, canvas: &mut WindowCanvas, renderer: &mut Renderer) -> UpdateResult {
if self.is_pending() {
return self.update_view(renderer);
}
if self.is_new_line() {
return UpdateResult::NoOp;
}
let config = renderer.config().editor_config();
let font_details =
FontDetails::new(config.font_path().as_str(), config.character_size().clone());
let font = renderer
.font_manager()
.load(&font_details)
.unwrap_or_else(|_| panic!("Could not load font for {:?}", font_details));
let c = self.text_character.clone();
let mut details = TextDetails {
text: c.to_string(),
color: self.color.clone(),
font: font_details.clone(),
};
if let Ok(texture) = renderer.texture_manager().load_text(&mut details, &font) {
renderer.render_texture(canvas, &texture, &self.source, &self.dest);
}
UpdateResult::NoOp
}
}
impl Update for TextCharacter {
fn update(&mut self, _ticks: i32) -> UpdateResult {
UpdateResult::NoOp
fn update(&mut self, _ticks: i32, _context: &UpdateContext) -> UR {
UR::NoOp
}
}
impl ClickHandler for TextCharacter {
fn on_left_click(&mut self, _point: &Point) -> UpdateResult {
UpdateResult::MoveCaret(
fn on_left_click(&mut self, _point: &Point, _context: &UpdateContext) -> UR {
UR::MoveCaret(
self.dest().clone(),
CaretPosition::new(self.position(), self.line(), 0),
)
}
fn is_left_click_target(&self, point: &Point) -> bool {
is_in_rect(point, self.dest())
fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool {
is_in_rect(
point,
&match context {
&UpdateContext::ParentPosition(p) => move_render_point(p.clone(), self.dest()),
_ => self.dest().clone(),
},
)
}
}
impl RenderBox for TextCharacter {
fn render_start_point(&self) -> Point {
self.dest.top_left()
}
}