继续Xed编辑器开发第二期:使用Rust从0到1写一个文本编辑器的开发进度,这是第三期的内容:
4.1 逐行清除
在每次刷新之前清除整个屏幕似乎不太理想,最好在重新绘制每行时清除每行。让我们删除 Clear(ClearType::All)
,而是在我们绘制的每行的末尾使用Clear(ClearType::UntilNewLine)
。
impl Output {fn new() -> Self {let win_size = terminal::size().map(|(x, y)| (x as usize, y as usize)).unwrap();Self {win_size,editor_contents: EditorContents::new(),}}fn clear_screen() -> crossterm::Result<()> {execute!(stdout(), terminal::Clear(ClearType::All))?;execute!(stdout(), cursor::MoveTo(0, 0))}fn draw_rows(&mut self) {let screen_rows = self.win_size.1;for i in 0..screen_rows {self.editor_contents.push('~');//add the followingqueue!(self.editor_contents,terminal::Clear(ClearType::UntilNewLine)).unwrap();//endif i < screen_rows - 1 {self.editor_contents.push_str("\r\n");}}}fn refresh_screen(&mut self) -> crossterm::Result<()> {//modifyqueue!(self.editor_contents, cursor::Hide, cursor::MoveTo(0, 0))?;self.draw_rows();queue!(self.editor_contents, cursor::MoveTo(0, 0), cursor::Show)?;self.editor_contents.flush()}
}
4.2 添加版本信息
是时候了,让我们简单地在屏幕下方的三分之一处显示编辑器的名称和版本。
const VERSION: &str = "0.0.1";impl Output{...fn draw_rows(&mut self) {let screen_rows = self.win_size.1;let screen_columns = self.win_size.0; // add this linefor i in 0..screen_rows {// add the followingif i == screen_rows / 3 {let mut welcome = format!("X Editor --- Version {}", VERSION);if welcome.len() > screen_columns {welcome.truncate(screen_columns)}self.editor_contents.push_str(&welcome);} else {self.editor_contents.push('~');}/* end */queue!(self.editor_contents,terminal::Clear(ClearType::UntilNewLine)).unwrap();if i < screen_rows - 1 {self.editor_contents.push_str("\r\n");}}}
}
我们使用
format!()
宏来加入VERSION
消息。然后检查长度是否大于屏幕一次可以显示的长度。如果大于,则将其截断。
- 现在处理下居中
impl Output {...fn draw_rows(&mut self) {let screen_rows = self.win_size.1;let screen_columns = self.win_size.0;for i in 0..screen_rows {if i == screen_rows / 3 {let mut welcome = format!("Pound Editor --- Version {}", VERSION);if welcome.len() > screen_columns {welcome.truncate(screen_columns)}/* add the following*/let mut padding = (screen_columns - welcome.len()) / 2;if padding != 0 {self.editor_contents.push('~');padding -= 1}(0..padding).for_each(|_| self.editor_contents.push(' '));self.editor_contents.push_str(&welcome);/* end */} else {self.editor_contents.push('~');}queue!(self.editor_contents,terminal::Clear(ClearType::UntilNewLine)).unwrap();if i < screen_rows - 1 {self.editor_contents.push_str("\r\n");}}}
}
使字符串居中,可以将屏幕宽度除以 2,然后从中减去字符串长度的一半。换言之:
screen_columns/2 - welcome.len()/2
,简化为(screen_columns - welcome.len()) / 2
。这告诉你应该从屏幕左边缘开始打印字符串的距离。因此,我们用空格字符填充该空间,除了第一个字符,它应该是波浪号
4.3 移动光标
现在让我们转到光标的控制上。目前,箭头键和其他任何键都不能移动游标。让我们从使用 wasd 键移动游标开始。
- 新建一个
CursorController
结构体来存储光标信息
struct CursorController {cursor_x: usize,cursor_y: usize,
}impl CursorController {fn new() -> CursorController {Self {cursor_x: 0,cursor_y: 0,}}
}
cursor_x
是光标的水平坐标(列),cursor_y
是垂直坐标(行)。我们将它们初始化为0
,因为我们希望光标从屏幕的左上角开始。
- 现在让我们向
Output
struct
and updaterefresh_screen()
添加一个cursor_controller
字段以使用cursor_x
和cursor_y
:
struct Output {win_size: (usize, usize),editor_contents: EditorContents,cursor_controller: CursorController, // add this field
}impl Output {fn new() -> Self {let win_size = terminal::size().map(|(x, y)| (x as usize, y as usize)).unwrap();Self {win_size,editor_contents: EditorContents::new(),cursor_controller: CursorController::new(), /* add initializer*/}}fn clear_screen() -> crossterm::Result<()> {execute!(stdout(), terminal::Clear(ClearType::All))?;execute!(stdout(), cursor::MoveTo(0, 0))}fn draw_rows(&mut self) {let screen_rows = self.win_size.1;let screen_columns = self.win_size.0;for i in 0..screen_rows {if i == screen_rows / 3 {let mut welcome = format!("Xed Editor --- Version {}", VERSION);if welcome.len() > screen_columns {welcome.truncate(screen_columns)}let mut padding = (screen_columns - welcome.len()) / 2;if padding != 0 {self.editor_contents.push('~');padding -= 1}(0..padding).for_each(|_| self.editor_contents.push(' '));self.editor_contents.push_str(&welcome);} else {self.editor_contents.push('~');}queue!(self.editor_contents,terminal::Clear(ClearType::UntilNewLine)).unwrap();if i < screen_rows - 1 {self.editor_contents.push_str("\r\n");}}}fn refresh_screen(&mut self) -> crossterm::Result<()> {queue!(self.editor_contents, cursor::Hide, cursor::MoveTo(0, 0))?;self.draw_rows();/* modify */let cursor_x = self.cursor_controller.cursor_x;let cursor_y = self.cursor_controller.cursor_y;queue!(self.editor_contents,cursor::MoveTo(cursor_x as u16, cursor_y as u16),cursor::Show)?;/* end */self.editor_contents.flush()}
}
- 现在我们添加一个
CursorController
方法来控制各种按键的移动逻辑:
impl CursorController {fn new() -> CursorController {Self {cursor_x: 0,cursor_y: 0,}}/* add this function */fn move_cursor(&mut self, direction: char) {match direction {'w' => {self.cursor_y -= 1;}'a' => {self.cursor_x -= 1;}'s' => {self.cursor_y += 1;}'d' => {self.cursor_x += 1;}_ => unimplemented!(),}}
}
这段逻辑很简单,就不过多解释了。
接下来是修改在Output
中使用该方法,因为我们希望通过这个struct
于所有的输出都有交互。
impl Output {...fn move_cursor(&mut self,direction:char) {self.cursor_controller.move_cursor(direction);}
}
- 修改
process_keyprocess()
,将按下的按键信息传递给move_cursor()
;
impl Editor {fn new() -> Self {Self {reader: Reader,output: Output::new(),}}fn process_keypress(&mut self) -> crossterm::Result<bool> { /* modify*/match self.reader.read_key()? {KeyEvent {code: KeyCode::Char('q'),modifiers: KeyModifiers::CONTROL,} => return Ok(false),/* add the following*/KeyEvent {code: KeyCode::Char(val @ ('w' | 'a' | 's' | 'd')),modifiers: KeyModifiers::NONE,} => self.output.move_cursor(val),// end_ => {}}Ok(true)}fn run(&mut self) -> crossterm::Result<bool> {self.output.refresh_screen()?;self.process_keypress()}
}
这里使用了
@
运算符。它的基本作用是创建一个变量并检查该变量是否提供了对于的匹配条件;因此在这种情况下它创建了
val
变量,然后检查该变量的取值是否满足给定的四个方向键的字符;所以,这段逻辑也类似于下面的写法:
fn process_keypress(&mut self) -> crossterm::Result<bool> {match self.reader.read_key()? {KeyEvent {code: KeyCode::Char('q'),modifiers: KeyModifiers::CONTROL,} => return Ok(false),/* note the following*/KeyEvent {code: KeyCode::Char(val),modifiers: KeyModifiers::NONE,} => {match val {'w'| 'a'|'s'|'d' => self.output.move_cursor(val),_=> {/*do nothing*/}}},// end_ => {}}Ok(true)}
现在如果你运行程序并移动光标可能会出现异常终止程序,这是由于溢出导致的
OutOfBounds
错误,后面会解决。
4.4 使用箭头移动光标
到这里为止,我们已经实现了指定字符按键的移动操作(尽管还有些BUG待修复),接下来就是实现方向键的移动控制功能。
实现上和上面的功能很类似,只需要对原代码进行简单的修改调整即可。
fn process_keypress(&mut self) -> crossterm::Result<bool> {/* modify*/match self.reader.read_key()? {KeyEvent {code: KeyCode::Char('q'),modifiers: KeyModifiers::CONTROL,} => return Ok(false),/* modify the following*/KeyEvent {code: direction @ (KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right),modifiers: KeyModifiers::NONE,} => self.output.move_cursor(direction),// end_ => {}}Ok(true)
}
impl CursorController {fn new() -> CursorController {Self {cursor_x: 0,cursor_y: 0,}}/* modify the function*/fn move_cursor(&mut self, direction: KeyCode) {match direction {KeyCode::Up => {self.cursor_y -= 1;}KeyCode::Left => {self.cursor_x -= 1;}KeyCode::Down => {self.cursor_y += 1;}KeyCode::Right => {self.cursor_x += 1;}_ => unimplemented!(),}}
}
impl Output {...fn move_cursor(&mut self, direction: KeyCode) { //modifyself.cursor_controller.move_cursor(direction);}...
}
4.5 修复光标移动时的越界问题
应该你还记得前面留下的一个BUG,如果记不得了就再去复习一遍,因为即使改用了方向键来移动光标,这个BUG依旧是存在的。
所以这小节主要就是解决这个问题来的。
会出现越界的异常,是因为我们定义的光标坐标的变量cursor_x
和cursor_y
类型是usize
,不能为负数。但这一点在我们移动时并不会得到保障,一旦移动导致负数的出现,那么程序就会panic
。
因为,解决这个问题的手段就是做一下边界判断,将BUG扼杀在摇篮之中。
struct CurSorController {cursor_x: usize,cursor_y: usize,screen_columns:usize,screen_rows:usize,
}
impl CursorController {/* modify */fn new(win_size: (usize, usize)) -> CursorController {Self {cursor_x: 0,cursor_y: 0,screen_columns: win_size.0,screen_rows: win_size.1,}}/* modify the function*/fn move_cursor(&mut self, direction: KeyCode) {match direction {KeyCode::Up => {self.cursor_y = self.cursor_y.saturating_sub(1);}KeyCode::Left => {if self.cursor_x != 0 {self.cursor_x -= 1;}}KeyCode::Down => {if self.cursor_y != self.screen_rows - 1 {self.cursor_y += 1;}}KeyCode::Right => {if self.cursor_x != self.screen_columns - 1 {self.cursor_x += 1;}}_ => unimplemented!(),}}
}
- 向上移动(Up):
- 使用
saturating_sub
方法来确保不会出现溢出,即当self.cursor_y
为 0 时,减去 1 后不会变为负数,而是保持为 0。- 向左移动(Left):
- 如果
self.cursor_x
不等于 0,则将self.cursor_x
减去 1。- 向下移动(Down):
- 如果
self.cursor_y
不等于self.screen_rows - 1
,则将self.cursor_y
加上 1,确保不会超出屏幕的底部。- 向右移动(Right):
- 如果
self.cursor_x
不等于self.screen_columns - 1
,则将self.cursor_x
加上 1,确保不会超出屏幕的右侧。
- 修改
Output
struct
:
impl Output {fn new() -> Self {let win_size = terminal::size().map(|(x, y)| (x as usize, y as usize)).unwrap();Self {win_size,editor_contents: EditorContents::new(),cursor_controller: CursorController::new(win_size), /* modify initializer*/}}...}
4.6 翻页和结束
本小节主要是实现上下翻页(快速跳页)以及首页末页的实现;
impl Editor {fn new() -> Self {Self {reader: Reader,output: Output::new(),}}fn process_keypress(&mut self) -> crossterm::Result<bool> {match self.reader.read_key()? {KeyEvent {code: KeyCode::Char('q'),modifiers: KeyModifiers::CONTROL,} => return Ok(false),KeyEvent {code:direction@(KeyCode::Up| KeyCode::Down| KeyCode::Left| KeyCode::Right| KeyCode::Home| KeyCode::End),modifiers: KeyModifiers::NONE,} => self.output.move_cursor(direction),KeyEvent {code: val @ (KeyCode::PageUp | KeyCode::PageDown),modifiers: KeyModifiers::NONE,} =>/*add this */ (0..self.output.win_size.1).for_each(|_| {self.output.move_cursor(if matches!(val, KeyCode::PageUp) {KeyCode::Up} else {KeyCode::Down});}),_ => {}}Ok(true)}fn run(&mut self) -> crossterm::Result<bool> {self.output.refresh_screen()?;self.process_keypress()}
}
如果您使用的是带有 Fn
按键的笔记本电脑,则可以按 Fn+↑
下并 Fn+↓
模拟按下 Page Up
和 Page Down
键。
对于Home
和End
的实现也很简单:
fn move_cursor(&mut self, direction: KeyCode) {match direction {KeyCode::Up => {self.cursor_y = self.cursor_y.saturating_sub(1);}KeyCode::Left => {if self.cursor_x != 0 {self.cursor_x -= 1;}}KeyCode::Down => {if self.cursor_y != self.screen_rows - 1 {self.cursor_y += 1;}}KeyCode::Right => {if self.cursor_x != self.screen_columns - 1 {self.cursor_x += 1;}}/* add the following*/KeyCode::End => self.cursor_x = self.screen_columns - 1,KeyCode::Home => self.cursor_x = 0,_ => unimplemented!(),}
}
如果您使用的是带有
Fn
键的笔记本电脑,则可以按Fn + ←
下Home
并Fn + →
模拟按下 和End
键。