继续Xed编辑器开发第二期:使用Rust从0到1写一个文本编辑器的开发进度,这是第三期的内容:
4.1 逐行清除
在每次刷新之前清除整个屏幕似乎不太理想,最好在重新绘制每行时清除每行。让我们删除
Clear(ClearType::All)
,而是在我们绘制的每行的末尾使用
Clear(ClearType::UntilNewLine)
。
implOutput{fnnew()->Self{let win_size =terminal::size().map(|(x, y)|(x asusize, y asusize)).unwrap();Self{
win_size,
editor_contents:EditorContents::new(),}}fnclear_screen()->crossterm::Result<()>{execute!(stdout(),terminal::Clear(ClearType::All))?;execute!(stdout(),cursor::MoveTo(0,0))}fndraw_rows(&mutself){let screen_rows =self.win_size.1;for i in0..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");}}}fnrefresh_screen(&mutself)->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 添加版本信息
是时候了,让我们简单地在屏幕下方的三分之一处显示编辑器的名称和版本。
constVERSION:&str="0.0.1";implOutput{...fndraw_rows(&mutself){let screen_rows =self.win_size.1;let screen_columns =self.win_size.0;// add this linefor i in0..screen_rows {// add the followingif i == screen_rows /3{letmut 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消息。然后检查长度是否大于屏幕一次可以显示的长度。如果大于,则将其截断。
- 现在处理下居中
implOutput{...fndraw_rows(&mutself){let screen_rows =self.win_size.1;let screen_columns =self.win_size.0;for i in0..screen_rows {if i == screen_rows /3{letmut welcome =format!("Pound Editor --- Version {}",VERSION);if welcome.len()> screen_columns {
welcome.truncate(screen_columns)}/* add the following*/letmut 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结构体来存储光标信息
structCursorController{
cursor_x:usize,
cursor_y:usize,}implCursorController{fnnew()->CursorController{Self{
cursor_x:0,
cursor_y:0,}}}
cursor_x是光标的水平坐标(列),
cursor_y是垂直坐标(行)。我们将它们初始化为
0,因为我们希望光标从屏幕的左上角开始。
- 现在让我们向
Output``````structand updaterefresh_screen()添加一个cursor_controller字段以使用cursor_x和cursor_y:
structOutput{
win_size:(usize,usize),
editor_contents:EditorContents,
cursor_controller:CursorController,// add this field}implOutput{fnnew()->Self{let win_size =terminal::size().map(|(x, y)|(x asusize, y asusize)).unwrap();Self{
win_size,
editor_contents:EditorContents::new(),
cursor_controller:CursorController::new(),/* add initializer*/}}fnclear_screen()->crossterm::Result<()>{execute!(stdout(),terminal::Clear(ClearType::All))?;execute!(stdout(),cursor::MoveTo(0,0))}fndraw_rows(&mutself){let screen_rows =self.win_size.1;let screen_columns =self.win_size.0;for i in0..screen_rows {if i == screen_rows /3{letmut welcome =format!("Xed Editor --- Version {}",VERSION);if welcome.len()> screen_columns {
welcome.truncate(screen_columns)}letmut 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");}}}fnrefresh_screen(&mutself)->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 asu16, cursor_y asu16),cursor::Show)?;/* end */self.editor_contents.flush()}}
- 现在我们添加一个
CursorController方法来控制各种按键的移动逻辑:
implCursorController{fnnew()->CursorController{Self{
cursor_x:0,
cursor_y:0,}}/* add this function */fnmove_cursor(&mutself, 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
于所有的输出都有交互。
implOutput{...fnmove_cursor(&mutself,direction:char){self.cursor_controller.move_cursor(direction);}}
- 修改
process_keyprocess(),将按下的按键信息传递给move_cursor();
implEditor{fnnew()->Self{Self{
reader:Reader,
output:Output::new(),}}fnprocess_keypress(&mutself)->crossterm::Result<bool>{/* modify*/matchself.reader.read_key()?{KeyEvent{
code:KeyCode::Char('q'),
modifiers:KeyModifiers::CONTROL,}=>returnOk(false),/* add the following*/KeyEvent{
code:KeyCode::Char(val @('w'|'a'|'s'|'d')),
modifiers:KeyModifiers::NONE,}=>self.output.move_cursor(val),// end
_ =>{}}Ok(true)}fnrun(&mutself)->crossterm::Result<bool>{self.output.refresh_screen()?;self.process_keypress()}}
- 这里使用了
@运算符。它的基本作用是创建一个变量并检查该变量是否提供了对于的匹配条件;- 因此在这种情况下它创建了
val变量,然后检查该变量的取值是否满足给定的四个方向键的字符;- 所以,这段逻辑也类似于下面的写法:
fnprocess_keypress(&mutself)->crossterm::Result<bool>{matchself.reader.read_key()?{KeyEvent{ code:KeyCode::Char('q'), modifiers:KeyModifiers::CONTROL,}=>returnOk(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待修复),接下来就是实现方向键的移动控制功能。
实现上和上面的功能很类似,只需要对原代码进行简单的修改调整即可。
fnprocess_keypress(&mutself)->crossterm::Result<bool>{/* modify*/matchself.reader.read_key()?{KeyEvent{
code:KeyCode::Char('q'),
modifiers:KeyModifiers::CONTROL,}=>returnOk(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)}
implCursorController{fnnew()->CursorController{Self{
cursor_x:0,
cursor_y:0,}}/* modify the function*/fnmove_cursor(&mutself, 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!(),}}}
implOutput{...fnmove_cursor(&mutself, direction:KeyCode){//modifyself.cursor_controller.move_cursor(direction);}...}
4.5 修复光标移动时的越界问题
应该你还记得前面留下的一个BUG,如果记不得了就再去复习一遍,因为即使改用了方向键来移动光标,这个BUG依旧是存在的。
所以这小节主要就是解决这个问题来的。
会出现越界的异常,是因为我们定义的光标坐标的变量
cursor_x
和
cursor_y
类型是
usize
,不能为负数。但这一点在我们移动时并不会得到保障,一旦移动导致负数的出现,那么程序就会
panic
。
因为,解决这个问题的手段就是做一下边界判断,将BUG扼杀在摇篮之中。
structCurSorController{
cursor_x:usize,
cursor_y:usize,
screen_columns:usize,
screen_rows:usize,}
implCursorController{/* modify */fnnew(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*/fnmove_cursor(&mutself, direction:KeyCode){match direction {KeyCode::Up=>{self.cursor_y =self.cursor_y.saturating_sub(1);}KeyCode::Left=>{ifself.cursor_x !=0{self.cursor_x -=1;}}KeyCode::Down=>{ifself.cursor_y !=self.screen_rows -1{self.cursor_y +=1;}}KeyCode::Right=>{ifself.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:
implOutput{fnnew()->Self{let win_size =terminal::size().map(|(x, y)|(x asusize, y asusize)).unwrap();Self{
win_size,
editor_contents:EditorContents::new(),
cursor_controller:CursorController::new(win_size),/* modify initializer*/}}...}
4.6 翻页和结束
本小节主要是实现上下翻页(快速跳页)以及首页末页的实现;
implEditor{fnnew()->Self{Self{
reader:Reader,
output:Output::new(),}}fnprocess_keypress(&mutself)->crossterm::Result<bool>{matchself.reader.read_key()?{KeyEvent{
code:KeyCode::Char('q'),
modifiers:KeyModifiers::CONTROL,}=>returnOk(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(ifmatches!(val,KeyCode::PageUp){KeyCode::Up}else{KeyCode::Down});}),
_ =>{}}Ok(true)}fnrun(&mutself)->crossterm::Result<bool>{self.output.refresh_screen()?;self.process_keypress()}}
如果您使用的是带有
Fn
按键的笔记本电脑,则可以按
Fn+↑
下并
Fn+↓
模拟按下
Page Up
和
Page Down
键。
对于
Home
和
End
的实现也很简单:
fnmove_cursor(&mutself, direction:KeyCode){match direction {KeyCode::Up=>{self.cursor_y =self.cursor_y.saturating_sub(1);}KeyCode::Left=>{ifself.cursor_x !=0{self.cursor_x -=1;}}KeyCode::Down=>{ifself.cursor_y !=self.screen_rows -1{self.cursor_y +=1;}}KeyCode::Right=>{ifself.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键。
版权归原作者 代号0408 所有, 如有侵权,请联系我们删除。