Add display config modal

This commit is contained in:
Adrian Woźniak 2019-06-04 22:46:39 +02:00
parent ae78642ca9
commit 33715a38c7
26 changed files with 696 additions and 141 deletions

View File

@ -13,6 +13,15 @@ rustup run nightly cargo build --all -rr
rustup run nightly cargo run -p rider-editor
```
## Keyboard mapping
* `DELETE` - delete next character
* `BACKSPACE` - delete prev character
* `SHIFT + DELETE` - delete line
* `CTRL + O` - open file
* `CTRL + S` - save current file
* `ESCAPE` - close current modal
## Road map
### v1.0
@ -25,10 +34,9 @@ rustup run nightly cargo run -p rider-editor
* [x] Open file menu
* [x] `Save file` with button
* [x] `Save file` with shortcut
* [ ] `Save file as...` with shortcut
* [x] Theme based menu UI
* [x] Lock scroll when no available content
* [ ] Config edit menu
* [x] Config edit menu
* [x] Project tree
* [x] Cover `rider` with tests at least 50%
* [x] Handle resize window

View File

@ -205,7 +205,7 @@ mod test_getters {
fn assert_menu_height() {
let config = Config::new();
let result = config.menu_height();
let expected = 60;
let expected = 40;
assert_eq!(result, expected);
}

View File

@ -16,7 +16,7 @@ pub struct AppState {
files: Vec<EditorFile>,
config: Arc<RwLock<Config>>,
file_editor: FileEditor,
open_file_modal: Option<OpenFile>,
modal: Option<ModalType>,
}
impl AppState {
@ -29,7 +29,7 @@ impl AppState {
),
files: vec![],
file_editor: FileEditor::new(Arc::clone(&config)),
open_file_modal: None,
modal: None,
config,
}
}
@ -65,13 +65,35 @@ impl AppState {
Ok(())
}
pub fn open_settings<R>(&mut self, renderer: &mut R) -> Result<(), String>
where
R: Renderer + CharacterSizeManager,
{
println!("Open Settings...");
match self.modal {
None => {
let mut settings = Settings::new(self.config.clone());
settings.prepare_ui(renderer);
self.modal = Some(ModalType::Settings(settings));
}
_ => return Ok(()),
}
Ok(())
}
pub fn close_modal(&mut self) -> Result<(), String> {
self.modal = None;
Ok(())
}
pub fn open_directory<R>(&mut self, dir_path: String, renderer: &mut R)
where
R: Renderer + CharacterSizeManager,
{
match self.open_file_modal.as_mut() {
Some(modal) => modal.open_directory(dir_path, renderer),
match self.modal.as_mut() {
Some(ModalType::OpenFile(modal)) => modal.open_directory(dir_path, renderer),
None => self.project_tree.open_directory(dir_path, renderer),
_ => (),
};
}
@ -86,19 +108,33 @@ impl AppState {
}
pub fn set_open_file_modal(&mut self, modal: Option<OpenFile>) {
self.open_file_modal = modal;
self.modal = if let Some(modal) = modal {
Some(ModalType::OpenFile(modal))
} else {
None
};
}
pub fn scroll_by(&mut self, x: i32, y: i32) {
if let Some(modal) = self.open_file_modal.as_mut() {
modal.scroll_by(x, y);
} else {
self.file_editor_mut().scroll_by(x, y);
}
match self.modal.as_mut() {
Some(ModalType::OpenFile(modal)) => modal.scroll_by(x, y),
Some(ModalType::Settings(modal)) => modal.scroll_by(x, y),
_ => self.file_editor_mut().scroll_by(x, y),
};
}
pub fn open_file_modal(&self) -> Option<&OpenFile> {
self.open_file_modal.as_ref()
match self.modal {
Some(ModalType::OpenFile(ref m)) => Some(m),
_ => None,
}
}
pub fn settings_modal(&self) -> Option<&Settings> {
match self.modal {
Some(ModalType::Settings(ref m)) => Some(m),
_ => None,
}
}
}
@ -119,9 +155,14 @@ impl AppState {
// project tree
self.project_tree.render(canvas, renderer);
// open file modal
match self.open_file_modal.as_ref() {
Some(modal) => modal.render(canvas, renderer, &RenderContext::Nothing),
// settings modal
match self.modal.as_ref() {
Some(ModalType::OpenFile(modal)) => {
return modal.render(canvas, renderer, &RenderContext::Nothing)
}
Some(ModalType::Settings(modal)) => {
return modal.render(canvas, renderer, &RenderContext::Nothing)
}
_ => (),
};
}
@ -136,10 +177,10 @@ impl AppState {
}
pub fn update(&mut self, ticks: i32, context: &UpdateContext) -> UpdateResult {
// open file modal
let res = match self.open_file_modal.as_mut() {
Some(modal) => modal.update(ticks, &UpdateContext::Nothing),
_ => UpdateResult::NoOp,
let res = match self.modal.as_mut() {
Some(ModalType::OpenFile(modal)) => modal.update(ticks, context.clone()),
Some(ModalType::Settings(modal)) => modal.update(ticks, context.clone()),
None => UpdateResult::NoOp,
};
if res != UpdateResult::NoOp {
return res;
@ -171,8 +212,13 @@ impl AppState {
.project_tree
.on_left_click(point, &UpdateContext::Nothing);
}
match self.open_file_modal.as_mut() {
Some(modal) => return modal.on_left_click(point, &UpdateContext::Nothing),
match self.modal.as_mut() {
Some(ModalType::OpenFile(modal)) => {
return modal.on_left_click(point, &UpdateContext::Nothing)
}
Some(ModalType::Settings(modal)) => {
return modal.on_left_click(point, &UpdateContext::Nothing)
}
_ => (),
};
if self
@ -188,10 +234,10 @@ impl AppState {
return UpdateResult::NoOp;
} else {
video_subsystem.text_input().start();
self.file_editor
return self
.file_editor
.on_left_click(point, &UpdateContext::Nothing);
}
UpdateResult::NoOp
}
pub fn is_left_click_target(&self, _point: &Point) -> bool {

View File

@ -3,6 +3,7 @@ pub use crate::renderer::CanvasRenderer;
use crate::ui::caret::{CaretPosition, MoveDirection};
use crate::ui::*;
pub use rider_config::{Config, ConfigAccess, ConfigHolder};
use sdl2::event::*;
use sdl2::hint;
use sdl2::keyboard::Keycode;
@ -24,7 +25,7 @@ use std::time::Duration;
pub type WindowCanvas = Canvas<Window>;
#[derive(PartialEq, Clone, Debug)]
#[derive(PartialEq, Debug)]
pub enum UpdateResult {
NoOp,
Stop,
@ -35,6 +36,7 @@ pub enum UpdateResult {
MoveCaret(Rect, CaretPosition),
DeleteFront,
DeleteBack,
DeleteLine,
Input(String),
InsertNewLine,
MoveCaretLeft,
@ -49,6 +51,8 @@ pub enum UpdateResult {
OpenFileModal,
FileDropped(String),
SaveCurrentFile,
OpenSettings,
CloseModal,
}
#[cfg_attr(tarpaulin, skip)]
@ -137,6 +141,7 @@ impl Application {
let res = app_state.on_left_click(&point, &mut self.video_subsystem);
match res {
UpdateResult::OpenDirectory(_) => new_tasks.push(res),
UpdateResult::OpenSettings => new_tasks.push(res),
UpdateResult::OpenFile(_) => {
new_tasks.push(res);
app_state.set_open_file_modal(None);
@ -154,9 +159,14 @@ impl Application {
UpdateResult::Input(text) => app_state
.file_editor_mut()
.insert_text(text.clone(), &mut renderer),
UpdateResult::InsertNewLine => {
app_state.file_editor_mut().insert_new_line(&mut renderer);
}
UpdateResult::InsertNewLine => app_state
.file_editor_mut()
.insert_new_line(&mut renderer)
.unwrap_or_else(|e| eprintln!("Failed to delete line {:?}", e)),
UpdateResult::DeleteLine => app_state
.file_editor_mut()
.delete_current_line(&mut renderer)
.unwrap_or_else(|e| eprintln!("Failed to delete line {:?}", e)),
UpdateResult::MoveCaretLeft => {
app_state.file_editor_mut().move_caret(MoveDirection::Left);
}
@ -205,6 +215,12 @@ impl Application {
UpdateResult::SaveCurrentFile => app_state
.save_file()
.unwrap_or_else(|e| eprintln!("Failed to save {:?}", e)),
UpdateResult::OpenSettings => app_state
.open_settings(&mut renderer)
.unwrap_or_else(|e| eprintln!("Failed to open settings {:?}", e)),
UpdateResult::CloseModal => app_state
.close_modal()
.unwrap_or_else(|e| eprintln!("Failed to close modal {:?}", e)),
}
}
self.tasks = new_tasks;
@ -266,12 +282,18 @@ impl Application {
.tasks
.push(UpdateResult::MouseDragStart(Point::new(x, y))),
Event::KeyDown { keycode, .. } if keycode.is_some() => match keycode.unwrap() {
Keycode::Escape => {
self.tasks.push(UpdateResult::CloseModal);
}
Keycode::Backspace => {
self.tasks.push(UpdateResult::DeleteFront);
}
Keycode::Delete => {
Keycode::Delete if !shift_pressed => {
self.tasks.push(UpdateResult::DeleteBack);
}
Keycode::Delete if shift_pressed => {
self.tasks.push(UpdateResult::DeleteLine);
}
Keycode::KpEnter | Keycode::Return => {
self.tasks.push(UpdateResult::InsertNewLine);
}
@ -307,9 +329,7 @@ impl Application {
MouseWheelDirection::Flipped => {
self.tasks.push(UpdateResult::Scroll { x, y: -y });
}
_ => {
// ignore
}
_ => (),
},
Event::Window {
win_event: WindowEvent::Resized(w, h),
@ -318,7 +338,7 @@ impl Application {
width: w,
height: h,
}),
_ => {}
_ => (),
}
}
}

View File

@ -104,14 +104,14 @@ where
file_editor.replace_current_file(new_file);
}
pub fn insert_new_line<R>(file_editor: &mut FileEditor, renderer: &mut R)
pub fn insert_new_line<R>(file_editor: &mut FileEditor, renderer: &mut R) -> Result<(), String>
where
R: ConfigHolder + CharacterSizeManager + Renderer,
{
let mut buffer: String = match file_editor.file() {
Some(file) => file.buffer(),
None => return,
};
let mut buffer: String = file_editor
.file()
.map(|file| file.buffer())
.ok_or_else(|| "No file is open".to_string())?;
let maybe_character = file_editor
.file()
@ -134,6 +134,36 @@ where
);
new_file.prepare_ui(renderer);
file_editor.replace_current_file(new_file);
Ok(())
}
pub fn delete_current_line<R>(file_editor: &mut FileEditor, renderer: &mut R) -> Result<(), String>
where
R: ConfigHolder + CharacterSizeManager + Renderer,
{
let file: &EditorFile = file_editor
.file()
.ok_or_else(|| "No file is open".to_string())?;
let mut new_buffer = String::new();
let target_line = file_editor.caret().line_number();
let mut current_line = 0;
for c in file.buffer_ref().chars() {
match c {
'\n' if current_line == target_line => {
current_line += 1;
}
'\n' => {
current_line += 1;
new_buffer.push(c);
}
_ if current_line == target_line => (),
_ => new_buffer.push(c),
}
}
let mut new_file = EditorFile::new(file.path(), new_buffer, file_editor.config().clone());
new_file.prepare_ui(renderer);
file_editor.replace_current_file(new_file);
Ok(())
}
#[cfg(test)]
@ -309,7 +339,8 @@ mod tests {
let mut renderer = RendererMock::new(config.clone());
let mut widget = FileEditor::new(config.clone());
widget.prepare_ui(&mut renderer);
widget.insert_new_line(&mut renderer);
let res: Result<(), String> = widget.insert_new_line(&mut renderer);
assert_eq!(res.is_ok(), false);
let expected = CaretPosition::new(0, 0, 0);
assert_eq!(widget.caret().position(), &expected);
let expected = Rect::new(0, 0, 6, 15);
@ -324,7 +355,8 @@ mod tests {
let file = EditorFile::new("".to_owned(), "".to_owned(), config.clone());
widget.open_file(file);
widget.prepare_ui(&mut renderer);
widget.insert_new_line(&mut renderer);
let res: Result<(), String> = widget.insert_new_line(&mut renderer);
assert_eq!(res.is_ok(), true);
let expected = CaretPosition::new(1, 1, 0);
assert_eq!(widget.caret().position(), &expected);
let expected = Rect::new(0, 13, 6, 15);
@ -339,7 +371,8 @@ mod tests {
let file = EditorFile::new("".to_owned(), "foo".to_owned(), config.clone());
widget.open_file(file);
widget.prepare_ui(&mut renderer);
widget.insert_new_line(&mut renderer);
let res: Result<(), String> = widget.insert_new_line(&mut renderer);
assert_eq!(res.is_ok(), true);
let expected = CaretPosition::new(1, 1, 0);
assert_eq!(widget.caret().position(), &expected);
let expected = Rect::new(0, 13, 6, 15);
@ -359,7 +392,8 @@ mod tests {
widget.prepare_ui(&mut renderer);
widget.move_caret(MoveDirection::Right);
widget.move_caret(MoveDirection::Right);
widget.insert_new_line(&mut renderer);
let res: Result<(), String> = widget.insert_new_line(&mut renderer);
assert_eq!(res.is_ok(), true);
let expected = CaretPosition::new(3, 1, 0);
assert_eq!(widget.caret().position(), &expected);
let expected = Rect::new(0, 13, 6, 15);

View File

@ -0,0 +1,5 @@
pub mod save_button;
pub mod settings_button;
pub use self::save_button::*;
pub use self::settings_button::*;

View File

@ -32,7 +32,7 @@ impl SaveButton {
{
use std::borrow::*;
let mut dest = match context {
&RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.dest),
&RenderContext::ParentPosition(p) => move_render_point(p.clone(), &self.dest),
_ => self.dest.clone(),
};
@ -78,8 +78,6 @@ impl SaveButton {
impl Update for SaveButton {
fn update(&mut self, _ticks: i32, _context: &UpdateContext) -> UR {
let config = self.config.read().unwrap();
self.dest.set_width(config.width());
UR::NoOp
}
}
@ -92,7 +90,7 @@ impl ClickHandler for SaveButton {
fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool {
match *context {
UpdateContext::ParentPosition(p) => move_render_point(p.clone(), &self.dest),
_ => self.dest(),
_ => self.dest,
}
.contains_point(point.clone())
}

View File

@ -32,7 +32,7 @@ impl SettingsButton {
{
use std::borrow::*;
let mut dest = match context {
&RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.dest),
&RenderContext::ParentPosition(p) => move_render_point(p.clone(), &self.dest),
_ => self.dest.clone(),
};
@ -78,21 +78,19 @@ impl SettingsButton {
impl Update for SettingsButton {
fn update(&mut self, _ticks: i32, _context: &UpdateContext) -> UR {
let config = self.config.read().unwrap();
self.dest.set_width(config.width());
UR::NoOp
}
}
impl ClickHandler for SettingsButton {
fn on_left_click(&mut self, _point: &Point, _context: &UpdateContext) -> UR {
UR::NoOp
UR::OpenSettings
}
fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool {
match *context {
UpdateContext::ParentPosition(p) => move_render_point(p.clone(), &self.dest),
_ => self.dest(),
_ => self.dest,
}
.contains_point(point.clone())
}

View File

@ -72,7 +72,7 @@ impl Caret {
use std::borrow::*;
let dest = match context.borrow() {
RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.dest),
RenderContext::ParentPosition(p) => move_render_point(p.clone(), &self.dest),
_ => self.dest().clone(),
};
let start = Point::new(dest.x(), dest.y());
@ -368,7 +368,7 @@ mod test_render {
#[test]
fn assert_render_line() {
let config = build_config();
let context = RenderContext::RelativePosition(Point::new(10, 14));
let context = RenderContext::ParentPosition(Point::new(10, 14));
let mut canvas = CanvasMock {
start: Point::new(0, 0),
end: Point::new(0, 0),

View File

@ -47,6 +47,10 @@ impl EditorFile {
self.buffer.clone()
}
pub fn buffer_ref(&self) -> &String {
&self.buffer
}
pub fn path(&self) -> String {
self.path.clone()
}

View File

@ -80,7 +80,8 @@ impl EditorFileSection {
}
}
fn iter_char(&self) -> EditorFileSectionIterator {
#[inline]
pub fn iter_char(&self) -> EditorFileSectionIterator {
EditorFileSectionIterator::new(self)
}

View File

@ -40,6 +40,7 @@ impl EditorFileToken {
}
}
#[inline]
pub fn characters(&self) -> &Vec<TextCharacter> {
&self.characters
}
@ -59,7 +60,8 @@ impl EditorFileToken {
}
}
fn iter_char(&self) -> EditorFileTokenIterator {
#[inline]
pub fn iter_char(&self) -> EditorFileTokenIterator {
EditorFileTokenIterator::new(self)
}
}
@ -82,12 +84,10 @@ impl TextWidget for EditorFileToken {
impl TextCollection for EditorFileToken {
fn get_character_at(&self, index: usize) -> Option<TextCharacter> {
for character in self.characters.iter() {
if character.position() == index {
return Some(character.clone());
}
}
None
self.characters
.iter()
.find(|character| character.position() == index)
.cloned()
}
fn get_line(&self, line: &usize) -> Option<Vec<&TextCharacter>> {

View File

@ -106,11 +106,18 @@ impl FileEditor {
file_content_manager::insert_text(self, text, renderer);
}
pub fn insert_new_line<R>(&mut self, renderer: &mut R)
pub fn insert_new_line<R>(&mut self, renderer: &mut R) -> Result<(), String>
where
R: ConfigHolder + CharacterSizeManager + Renderer,
{
file_content_manager::insert_new_line(self, renderer);
file_content_manager::insert_new_line(self, renderer)
}
pub fn delete_current_line<R>(&mut self, renderer: &mut R) -> Result<(), String>
where
R: ConfigHolder + CharacterSizeManager + Renderer,
{
file_content_manager::delete_current_line(self, renderer)
}
fn is_text_character_clicked(&self, point: &Point) -> bool {
@ -264,22 +271,18 @@ impl FileEditor {
Some(file) => file.render(
canvas,
renderer,
&RenderContext::RelativePosition(self.render_start_point()),
&RenderContext::ParentPosition(self.render_start_point()),
),
_ => (),
};
self.caret.render(
canvas,
&RenderContext::RelativePosition(self.render_start_point()),
);
self.vertical_scroll_bar.render(
canvas,
&RenderContext::RelativePosition(self.dest.top_left()),
);
self.horizontal_scroll_bar.render(
canvas,
&RenderContext::RelativePosition(self.dest.top_left()),
&RenderContext::ParentPosition(self.render_start_point()),
);
self.vertical_scroll_bar
.render(canvas, &RenderContext::ParentPosition(self.dest.top_left()));
self.horizontal_scroll_bar
.render(canvas, &RenderContext::ParentPosition(self.dest.top_left()));
}
pub fn prepare_ui<T>(&mut self, renderer: &mut T)

View File

@ -252,12 +252,12 @@ impl DirectoryView {
self.icon_height as i32 + CHILD_MARGIN,
);
for dir in self.directories.iter() {
let context = RenderContext::RelativePosition(point.clone());
let context = RenderContext::ParentPosition(point.clone());
dir.render(canvas, renderer, &context);
point = point + Point::new(0, dir.height() as i32 + CHILD_MARGIN as i32);
}
for file in self.files.iter() {
let context = RenderContext::RelativePosition(point.clone());
let context = RenderContext::ParentPosition(point.clone());
file.render(canvas, renderer, &context);
point = point + Point::new(0, file.height() as i32 + CHILD_MARGIN as i32);
}
@ -302,7 +302,7 @@ impl DirectoryView {
{
let dest = self.dest();
let move_point = match context {
&RenderContext::RelativePosition(p) => p.clone(),
&RenderContext::ParentPosition(p) => p.clone(),
_ => Point::new(0, 0),
};
let mut dest = move_render_point(move_point, &dest);

View File

@ -139,7 +139,7 @@ impl FileEntry {
R: Renderer,
{
let mut dest = match context {
&RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.dest),
&RenderContext::ParentPosition(p) => move_render_point(p.clone(), &self.dest),
_ => self.dest.clone(),
};
self.render_icon(canvas, renderer, &mut dest);

View File

@ -0,0 +1,121 @@
use crate::app::*;
use crate::renderer::*;
use crate::ui::*;
use sdl2::pixels::Color;
use sdl2::rect::{Point, Rect};
use std::collections::HashMap;
const DEST_WIDTH: u32 = 16;
const DEST_HEIGHT: u32 = 16;
const SRC_WIDTH: u32 = 64;
const SRC_HEIGHT: u32 = 64;
pub struct Label {
name_width: u32,
height: u32,
name: String,
source: Rect,
dest: Rect,
char_sizes: HashMap<char, Rect>,
config: ConfigAccess,
}
impl Label {
pub fn new(name: String, config: ConfigAccess) -> Self {
Self {
name,
name_width: 0,
height: 0,
dest: Rect::new(0, 0, DEST_WIDTH, DEST_HEIGHT),
source: Rect::new(0, 0, SRC_WIDTH, SRC_HEIGHT),
config,
char_sizes: HashMap::new(),
}
}
pub fn name_width(&self) -> u32 {
self.name_width
}
pub fn name(&self) -> String {
self.name.clone()
}
pub fn render<C, R>(&self, canvas: &mut C, renderer: &mut R, context: &RenderContext)
where
C: CanvasAccess,
R: Renderer,
{
let dest = match context {
&RenderContext::ParentPosition(p) => move_render_point(p.clone(), &self.dest),
_ => self.dest.clone(),
};
let mut d = dest.clone();
d.set_x(dest.x() + NAME_MARGIN);
let font_details = build_font_details(self);
for c in self.name.chars() {
let size = self
.char_sizes
.get(&c)
.unwrap_or(&Rect::new(0, 0, 0, 0))
.clone();
let mut text_details = TextDetails {
color: Color::RGBA(255, 255, 255, 0),
text: c.to_string(),
font: font_details.clone(),
};
let maybe_texture = renderer.load_text_tex(&mut text_details, font_details.clone());
if let Ok(texture) = maybe_texture {
d.set_width(size.width());
d.set_height(size.height());
canvas
.render_image(texture, self.source.clone(), d.clone())
.unwrap_or_else(|_| panic!("Failed to draw directory entry texture"));
d.set_x(d.x() + size.width() as i32);
}
}
}
pub fn prepare_ui<R>(&mut self, renderer: &mut R)
where
R: Renderer + CharacterSizeManager,
{
let w_rect = renderer.load_character_size('W');
self.char_sizes.insert('W', w_rect.clone());
self.height = w_rect.height();
self.name_width = 0;
for c in self.name().chars() {
let size = { renderer.load_character_size(c.clone()) };
self.char_sizes.insert(c, size);
self.name_width += size.width();
}
self.dest.set_width(w_rect.height());
self.dest.set_height(w_rect.height());
}
}
impl RenderBox for Label {
fn render_start_point(&self) -> Point {
self.dest.top_left()
}
fn dest(&self) -> Rect {
self.dest.clone()
}
}
impl Update for Label {
fn update(&mut self, _ticks: i32, _context: &UpdateContext) -> UpdateResult {
UpdateResult::NoOp
}
}
impl ConfigHolder for Label {
fn config(&self) -> &ConfigAccess {
&self.config
}
}

View File

@ -50,7 +50,7 @@ impl MenuBar {
use std::borrow::*;
let relative_position = match context.borrow() {
RenderContext::RelativePosition(p) => p.clone(),
RenderContext::ParentPosition(p) => p.clone(),
_ => Point::new(0, 0),
};
@ -64,21 +64,19 @@ impl MenuBar {
canvas
.render_border(
match context.borrow() {
RenderContext::RelativePosition(p) => {
move_render_point((*p).clone(), &self.dest)
}
RenderContext::ParentPosition(p) => move_render_point((*p).clone(), &self.dest),
_ => self.dest(),
},
self.border_color.clone(),
)
.unwrap_or_else(|_| panic!("Failed to draw main menu background"));
let context = RenderContext::RelativePosition(
let context = RenderContext::ParentPosition(
relative_position.offset(SAVE_BUTTON_OFFSET_LEFT, SAVE_BUTTON_OFFSET_TOP),
);
self.save_button.render(canvas, renderer, &context);
let context = RenderContext::RelativePosition(
let context = RenderContext::ParentPosition(
relative_position.offset(SAVE_BUTTON_OFFSET_LEFT * 2, SAVE_BUTTON_OFFSET_TOP),
);
self.settings_button.render(canvas, renderer, &context);
@ -324,12 +322,12 @@ mod test_render {
let mut widget = MenuBar::new(Arc::clone(&config));
widget.prepare_ui();
widget.render(&mut canvas, &mut renderer, &context);
assert_eq!(widget.dest(), Rect::new(0, 0, 1024, 60));
assert_eq!(widget.dest(), Rect::new(0, 0, 1024, 40));
let expected = CanvasMock {
clipping: Rect::new(0, 0, 1024, 60),
background_rect: Rect::new(0, 0, 1024, 60),
clipping: Rect::new(32, 10, 32, 32),
background_rect: Rect::new(0, 0, 1024, 40),
background_color: Color::RGBA(18, 18, 18, 0),
border_rect: Rect::new(0, 0, 1024, 60),
border_rect: Rect::new(0, 0, 1024, 40),
border_color: Color::RGBA(200, 200, 200, 0),
};
assert_eq!(canvas, expected);
@ -340,6 +338,6 @@ mod test_render {
let config = support::build_config();
let mut widget = MenuBar::new(Arc::clone(&config));
widget.prepare_ui();
assert_eq!(widget.dest(), Rect::new(0, 0, 1024, 60));
assert_eq!(widget.dest(), Rect::new(0, 0, 1024, 40));
}
}

View File

@ -8,29 +8,29 @@ use crate::app::UpdateResult as UR;
use crate::renderer::managers::*;
use rider_config::*;
pub mod buttons;
pub mod caret;
pub mod file;
pub mod file_editor;
pub mod filesystem;
pub mod label;
pub mod menu_bar;
pub mod modal;
pub mod project_tree;
pub mod save_button;
pub mod scroll_bar;
pub mod settings_button;
pub mod text_character;
pub use crate::ui::caret::*;
pub use crate::ui::file::*;
pub use crate::ui::file_editor::*;
pub use crate::ui::filesystem::*;
pub use crate::ui::menu_bar::*;
pub use crate::ui::modal::*;
pub use crate::ui::project_tree::*;
pub use crate::ui::save_button::*;
pub use crate::ui::scroll_bar::*;
pub use crate::ui::settings_button::*;
pub use crate::ui::text_character::*;
pub use self::buttons::*;
pub use self::caret::*;
pub use self::file::*;
pub use self::file_editor::*;
pub use self::filesystem::*;
pub use self::label::*;
pub use self::menu_bar::*;
pub use self::modal::*;
pub use self::project_tree::*;
pub use self::scroll_bar::*;
pub use self::text_character::*;
#[derive(Debug)]
pub enum UpdateContext<'l> {
@ -42,7 +42,7 @@ pub enum UpdateContext<'l> {
#[derive(Clone, PartialEq, Debug)]
pub enum RenderContext {
Nothing,
RelativePosition(Point),
ParentPosition(Point),
}
pub trait CanvasAccess {

View File

@ -1,3 +1,30 @@
pub mod open_file;
pub mod settings;
pub use crate::ui::modal::open_file::OpenFile;
pub use self::open_file::*;
pub use self::settings::*;
pub enum ModalType {
OpenFile(OpenFile),
Settings(Settings),
}
impl PartialEq for ModalType {
fn eq(&self, other: &ModalType) -> bool {
match (self, other) {
(ModalType::OpenFile { .. }, ModalType::OpenFile { .. }) => true,
(ModalType::Settings { .. }, ModalType::Settings { .. }) => true,
_ => false,
}
}
}
impl std::fmt::Debug for ModalType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
let name = match self {
ModalType::OpenFile(_) => "OpenFile",
ModalType::Settings(_) => "Settings",
};
write!(f, "<Modal::{:?} {{}}", name)
}
}

View File

@ -1,3 +1,4 @@
use crate::app::UpdateResult as UR;
use crate::renderer::renderer::Renderer;
use crate::ui::*;
use crate::ui::{RenderContext as RC, UpdateContext as UC};
@ -151,7 +152,7 @@ impl OpenFile {
R: Renderer + CharacterSizeManager + ConfigHolder,
{
let dest = match context {
RC::RelativePosition(p) => move_render_point(p.clone(), &self.dest),
RC::ParentPosition(p) => move_render_point(p.clone(), &self.dest),
_ => self.dest.clone(),
};
@ -164,7 +165,7 @@ impl OpenFile {
.render_border(dest, self.border_color)
.unwrap_or_else(|_| panic!("Failed to render open file modal border!"));
let context = RC::RelativePosition(
let context = RC::ParentPosition(
dest.top_left() + Point::new(CONTENT_MARGIN_LEFT, CONTENT_MARGIN_TOP) + self.scroll(),
);
@ -172,14 +173,10 @@ impl OpenFile {
self.directory_view.render(canvas, renderer, &context);
// Scroll bars
self.vertical_scroll_bar.render(
canvas,
&RenderContext::RelativePosition(self.dest.top_left()),
);
self.horizontal_scroll_bar.render(
canvas,
&RenderContext::RelativePosition(self.dest.top_left()),
);
self.vertical_scroll_bar
.render(canvas, &RenderContext::ParentPosition(self.dest.top_left()));
self.horizontal_scroll_bar
.render(canvas, &RenderContext::ParentPosition(self.dest.top_left()));
}
pub fn prepare_ui<R>(&mut self, renderer: &mut R)
@ -397,7 +394,7 @@ mod tests {
let mut canvas = CanvasMock::new();
let widget = OpenFile::new(path.to_owned(), 100, 100, config);
let p = Point::new(100, 100);
let context = RenderContext::RelativePosition(p);
let context = RenderContext::ParentPosition(p);
widget.render(&mut canvas, &mut renderer, &context);
}

View File

@ -0,0 +1,238 @@
use crate::app::UpdateResult as UR;
use crate::renderer::renderer::Renderer;
use crate::ui::*;
use crate::ui::{RenderContext as RC, UpdateContext as UC};
use rider_config::ConfigAccess;
use rider_config::ConfigHolder;
use sdl2::pixels::Color;
use sdl2::rect::{Point, Rect};
use std::sync::Arc;
const CONTENT_MARGIN_LEFT: i32 = 16;
const CONTENT_MARGIN_TOP: i32 = 24;
const DEFAULT_ICON_SIZE: u32 = 16;
pub struct Settings {
vertical_scroll_bar: VerticalScrollBar,
horizontal_scroll_bar: HorizontalScrollBar,
dest: Rect,
full_dest: Rect,
background_color: Color,
border_color: Color,
config: ConfigAccess,
font_label: Label,
font_value: Label,
}
impl Settings {
pub fn new(config: ConfigAccess) -> Self {
// let (window_width, window_height, background_color, border_color) = {
let c = config
.read()
.unwrap_or_else(|_| panic!("Failed to read config"));
let theme = c.theme();
let window_width = c.width();
let window_height = c.height();
let background_color = theme.background().into();
let border_color = theme.border_color().into();
// };
Self {
vertical_scroll_bar: VerticalScrollBar::new(Arc::clone(&config)),
horizontal_scroll_bar: HorizontalScrollBar::new(Arc::clone(&config)),
dest: Rect::new(
CONTENT_MARGIN_LEFT,
CONTENT_MARGIN_TOP,
window_width - (CONTENT_MARGIN_LEFT * 2) as u32,
window_height - (CONTENT_MARGIN_TOP * 2) as u32,
),
full_dest: Rect::new(0, 0, DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE),
background_color,
border_color,
font_label: Label::new("Font path".into(), config.clone()),
font_value: Label::new(c.editor_config().font_path().clone(), config.clone()),
config: config.clone(),
}
}
pub fn full_rect(&self) -> &Rect {
&self.full_dest
}
pub fn scroll_by(&mut self, x: i32, y: i32) {
let read_config = self.config.read().unwrap();
let value_x = read_config.scroll().speed() * x;
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);
}
}
}
pub fn scroll(&self) -> Point {
Point::new(
-self.horizontal_scroll_bar.scroll_value(),
-self.vertical_scroll_bar.scroll_value(),
)
}
pub fn render<C, R>(&self, canvas: &mut C, renderer: &mut R, context: &RC)
where
C: CanvasAccess,
R: Renderer + CharacterSizeManager + ConfigHolder,
{
let dest = match context {
RC::ParentPosition(p) => move_render_point(p.clone(), &self.dest),
_ => self.dest.clone(),
};
// Background
canvas.set_clipping(dest.clone());
canvas
.render_rect(dest, self.background_color)
.unwrap_or_else(|_| panic!("Failed to render open file modal background!"));
canvas
.render_border(dest, self.border_color)
.unwrap_or_else(|_| panic!("Failed to render open file modal border!"));
self.font_label.render(
canvas,
renderer,
&RC::ParentPosition(
self.render_start_point()
+ Point::new(CONTENT_MARGIN_LEFT, CONTENT_MARGIN_TOP)
+ self.scroll(),
),
);
self.font_value.render(
canvas,
renderer,
&RC::ParentPosition(
self.render_start_point()
+ Point::new(
(CONTENT_MARGIN_LEFT * 2) + self.font_label.name_width() as i32,
CONTENT_MARGIN_TOP,
)
+ self.scroll(),
),
);
// Scroll bars
self.vertical_scroll_bar
.render(canvas, &RenderContext::ParentPosition(self.dest.top_left()));
self.horizontal_scroll_bar
.render(canvas, &RenderContext::ParentPosition(self.dest.top_left()));
}
pub fn prepare_ui<R>(&mut self, renderer: &mut R)
where
R: Renderer + CharacterSizeManager,
{
self.font_label.prepare_ui(renderer);
self.font_value.prepare_ui(renderer);
}
}
impl ClickHandler for Settings {
fn on_left_click(&mut self, _point: &Point, context: &UpdateContext) -> UR {
let dest = match context {
UC::ParentPosition(p) => move_render_point(*p, &self.dest),
_ => self.dest,
};
let _context = UC::ParentPosition(
dest.top_left() + Point::new(CONTENT_MARGIN_LEFT, CONTENT_MARGIN_TOP) + self.scroll(),
);
// let res = self.directory_view.on_left_click(point, &context);
// {
// let dest = self.directory_view.dest();
// let full_dest = Rect::new(
// dest.x(),
// dest.y(),
// dest.width() + (2 * CONTENT_MARGIN_LEFT as u32),
// dest.height() + (2 * CONTENT_MARGIN_TOP as u32),
// );
// self.full_dest = full_dest;
// }
// res
UR::NoOp
}
fn is_left_click_target(&self, _point: &Point, context: &UpdateContext) -> bool {
let dest = match *context {
UC::ParentPosition(p) => move_render_point(p.clone(), &self.dest),
_ => self.dest.clone(),
};
let p =
dest.top_left() + Point::new(CONTENT_MARGIN_LEFT, CONTENT_MARGIN_TOP) + self.scroll();
let _context = UC::ParentPosition(p);
// if self.directory_view.is_left_click_target(point, &context) {
// true
// } else {
// Rect::new(p.x(), p.y(), dest.width(), dest.height()).contains_point(point.clone())
// }
false
}
}
impl RenderBox for Settings {
fn render_start_point(&self) -> Point {
self.dest.top_left()
}
fn dest(&self) -> Rect {
self.dest.clone()
}
}
impl Update for Settings {
fn update(&mut self, ticks: i32, context: &UpdateContext) -> UR {
let (window_width, window_height, color, scroll_width, scroll_margin) = {
let c = self.config.read().unwrap();
(
c.width(),
c.height(),
c.theme().background().into(),
c.scroll().width(),
c.scroll().margin_right(),
)
};
self.dest.set_x(CONTENT_MARGIN_LEFT);
self.dest
.set_width(window_width - (CONTENT_MARGIN_LEFT * 2) as u32);
self.dest.set_y(CONTENT_MARGIN_TOP);
self.dest
.set_height(window_height - (CONTENT_MARGIN_TOP * 2) as u32);
self.background_color = color;
// Scroll bars
self.vertical_scroll_bar
.set_full_size(self.full_dest.height()); // full dest
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_dest.width()); // full dest
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);
// End
UR::NoOp
}
}

View File

@ -98,7 +98,7 @@ impl ProjectTreeSidebar {
.unwrap();
// dir view
let context = RenderContext::RelativePosition(
let context = RenderContext::ParentPosition(
self.dest.top_left() + Point::new(CONTENT_MARGIN_LEFT, CONTENT_MARGIN_TOP),
);
self.dir_view.render(canvas, renderer, &context);
@ -271,7 +271,7 @@ mod tests {
let mut renderer = RendererMock::new(config.clone());
let mut widget = ProjectTreeSidebar::new("/tmp".to_owned(), config);
widget.prepare_ui(&mut renderer);
assert_eq!(widget.full_rect(), Rect::new(0, 60, 200, 860));
assert_eq!(widget.full_rect(), Rect::new(0, 40, 200, 860));
}
#[test]
@ -279,7 +279,7 @@ mod tests {
let config = build_config();
let mut widget = ProjectTreeSidebar::new("/tmp".to_owned(), config);
widget.update(0);
assert_eq!(widget.full_rect(), Rect::new(0, 60, 200, 800));
assert_eq!(widget.full_rect(), Rect::new(0, 40, 200, 820));
}
#[test]
@ -288,7 +288,7 @@ mod tests {
let mut renderer = RendererMock::new(config.clone());
let mut widget = ProjectTreeSidebar::new("/tmp".to_owned(), config);
widget.prepare_ui(&mut renderer);
assert_eq!(widget.full_rect(), Rect::new(0, 60, 200, 860));
assert_eq!(widget.full_rect(), Rect::new(0, 40, 200, 860));
}
#[test]

View File

@ -66,7 +66,7 @@ impl HorizontalScrollBar {
canvas
.render_rect(
match context {
RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.rect),
RenderContext::ParentPosition(p) => move_render_point(p.clone(), &self.rect),
_ => self.rect.clone(),
},
Color::RGBA(255, 255, 255, 0),

View File

@ -66,7 +66,7 @@ impl VerticalScrollBar {
canvas
.render_border(
match context {
RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.rect),
RenderContext::ParentPosition(p) => move_render_point(p.clone(), &self.rect),
_ => self.rect.clone(),
},
Color::RGBA(255, 255, 255, 0),

View File

@ -124,7 +124,7 @@ impl TextCharacter {
font: font_details.clone(),
};
let dest = match context {
RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.dest),
RenderContext::ParentPosition(p) => move_render_point(p.clone(), &self.dest),
_ => self.dest(),
};

View File

@ -28,7 +28,7 @@ fn main() -> std::io::Result<()> {
mod tests {
use super::*;
use std::env::set_var;
use std::fs::create_dir_all;
use std::fs::{create_dir_all, remove_dir_all};
use std::path::Path;
use uuid::Uuid;
@ -48,73 +48,130 @@ mod tests {
let uniq = Uuid::new_v4();
let joined = join("/tmp/rider-tests".to_owned(), uniq.to_string());
let test_path = joined.as_str();
remove_dir_all(joined.clone()).unwrap_or_else(|_| ());
create_dir_all(test_path.to_owned()).unwrap();
set_var("XDG_CONFIG_HOME", test_path);
set_var("XDG_RUNTIME_DIR", test_path);
assert_eq!(exists(&test_path.to_owned(), ".rider"), false);
assert_eq!(main().is_ok(), true);
assert_eq!(exists(&test_path.to_owned(), ".rider"), true);
debug_assert!(
!exists(&test_path.to_owned(), ".rider"),
"rider config dir should not exists before generator run"
);
debug_assert!(main().is_ok(), "generator should not failed");
debug_assert!(
exists(&test_path.to_owned(), ".rider"),
"rider config dir should exists after generator run"
);
}
#[test]
fn assert_fonts_dir() {
let uniq = Uuid::new_v4();
let joined = join("/tmp/rider-tests".to_owned(), uniq.to_string());
remove_dir_all(joined.clone()).unwrap_or_else(|_| ());
create_dir_all(joined.clone()).unwrap();
set_var("XDG_CONFIG_HOME", joined.as_str().clone());
set_var("XDG_RUNTIME_HOME", joined.as_str().clone());
assert_eq!(exists(&joined, "rider/fonts"), false);
assert_eq!(main().is_ok(), true);
assert_eq!(exists(&joined, "rider/fonts"), true);
debug_assert!(
!exists(&joined, "rider/fonts"),
"fonts director should not exists before run generator"
);
debug_assert!(main().is_ok(), "generator should not failed");
debug_assert!(
exists(&joined, "rider/fonts"),
"fonts director should exists after run generator"
);
}
#[test]
fn assert_log_dir() {
let uniq = Uuid::new_v4();
let joined = join("/tmp/rider-tests".to_owned(), uniq.to_string());
remove_dir_all(joined.clone()).unwrap_or_else(|_| ());
create_dir_all(joined.clone()).unwrap();
set_var("XDG_CONFIG_HOME", joined.as_str().clone());
set_var("XDG_RUNTIME_HOME", joined.as_str().clone());
assert_eq!(exists(&joined, "rider/log"), false);
assert_eq!(main().is_ok(), true);
assert_eq!(exists(&joined, "rider/log"), true);
debug_assert!(
!exists(&joined, "rider/log"),
"log should not exists before run generator"
);
debug_assert!(main().is_ok(), "generator should not failed");
debug_assert!(
exists(&joined, "rider/log"),
"log should exists after run generator"
);
}
#[test]
fn assert_themes_dir() {
let uniq = Uuid::new_v4();
let joined = join("/tmp/rider-tests".to_owned(), uniq.to_string());
remove_dir_all(joined.clone()).unwrap_or_else(|_| ());
create_dir_all(joined.clone()).unwrap();
set_var("XDG_CONFIG_HOME", joined.as_str().clone());
set_var("XDG_RUNTIME_HOME", joined.as_str().clone());
assert_eq!(exists(&joined, "rider/themes"), false);
assert_eq!(main().is_ok(), true);
assert_eq!(exists(&joined, "rider/themes"), true);
debug_assert!(
!exists(&joined, "rider/themes"),
"themes should not exists before run generator"
);
debug_assert!(main().is_ok(), "generator should not failed");
debug_assert!(
exists(&joined, "rider/themes"),
"themes should exists after run generator"
);
}
#[test]
fn assert_default_json() {
let uniq = Uuid::new_v4();
let joined = join("/tmp/rider-tests".to_owned(), uniq.to_string());
remove_dir_all(joined.clone()).unwrap_or_else(|_| ());
create_dir_all(joined.clone()).unwrap();
set_var("XDG_CONFIG_HOME", joined.as_str().clone());
set_var("XDG_RUNTIME_HOME", joined.as_str().clone());
assert_eq!(exists(&joined, "rider/themes/default.json"), false);
assert_eq!(main().is_ok(), true);
assert_eq!(exists(&joined, "rider/themes/default.json"), true);
debug_assert!(
!exists(&joined, "rider/themes/default.json"),
"default theme should not exists before run generator"
);
debug_assert!(main().is_ok(), "generator should not failed");
debug_assert!(
exists(&joined, "rider/themes/default.json"),
"default theme should exists after run generator"
);
}
#[test]
fn assert_railscasts_json() {
let uniq = Uuid::new_v4();
let joined = join("/tmp/rider-tests".to_owned(), uniq.to_string());
remove_dir_all(joined.clone()).unwrap_or_else(|_| ());
create_dir_all(joined.clone()).unwrap();
set_var("XDG_CONFIG_HOME", joined.as_str().clone());
set_var("XDG_RUNTIME_HOME", joined.as_str().clone());
assert_eq!(exists(&joined, "rider/themes/railscasts.json"), false);
assert_eq!(main().is_ok(), true);
assert_eq!(exists(&joined, "rider/themes/railscasts.json"), true);
debug_assert!(
!exists(&joined, "rider/themes/railscasts.json"),
"railscasts theme should not exists before run generator"
);
debug_assert!(main().is_ok(), "generator should not failed");
debug_assert!(
exists(&joined, "rider/themes/railscasts.json"),
"railscasts theme should exists after run generator"
);
}
}