跳转至

New and Save

本节添加新建和保存文件的功能。

首先,在EditorEdit消息处理中检查是否对文件进行了修改,并且记录文件的修改状态。

struct Editor {
    // ...
    modified: bool
}
// ... In `update` function
            Message::EditorEdit(action) => {
                match &action {
                    text_editor::Action::Edit(_) => self.modified = true,
                    _ => {}
                }
                self.content.edit(action);
                Command::none()
            },
            Message::FileOpened(Ok((path, result))) => {
                self.path = Some(path);
                self.modified = false;
                self.content = text_editor::Content::with(&result);
                Command::none()
            },
            // ...
            Message::OpenButtonPressed => {
                self.modified = false;
                Command::perform(pick_file(), Message::FileOpened)
            },

其次,调整文件路径控件的不同显示状态:

  1. 当文件路径为空时,为新文件,显示“New File”。
  2. 当打开一个文件时,显示文件路径。
  3. 当文件被修改后,文件路径后加上“*”。
  4. 当打开文件出错时,显示错误信息。
let path_indicator = if let Some(error) = &self.error {
    match error {
        Error::DialogClosed => text("Dialog closed"),
        Error::IO(kind) => text(format!("I/O error: {:?}", kind))
    }
} else {
    let path_text = match &self.path {
        None => String::from("New file"),
        Some(path) => path.to_string_lossy().to_string()
    };
    let suffix = if self.modified { "*" } else { "" };
    text(format!("{}{}", path_text, suffix))
};

在创建文件时,需要清空文件路径和内容,以及清空错误信息。加入一个新的消息类型NewButtonPressed,由一个按钮触发,在update函数中执行这个逻辑。

enum Message {
    EditorEdit(text_editor::Action),
    FileOpened(Result<(PathBuf, Arc<String>), Error>),
    NewButtonPressed,
    OpenButtonPressed
}
// ... In `view` function
let controls = row![
    button("New").on_press(Message::NewButtonPressed),
    button("Open").on_press(Message::OpenButtonPressed)
];
// ... In matching logic in `update` function
Message::NewButtonPressed => {
    self.content = text_editor::Content::new();
    self.error = None;
    self.path = None;
    self.modified = false;
    Command::none()
},
// ...

接下来处理保存文件的逻辑,当存在文件路径时,保存文件,否则打开文件选择对话框。

async fn save_file(path: Option<PathBuf>, content: String) -> Result<PathBuf, Error> {
    let path = if let Some(path) = path {
        path
    } else {
        rfd::AsyncFileDialog::new()
            .set_title("Save the file to...")
            .save_file()
            .await
            .ok_or(Error::DialogClosed)?
            .path()
            .to_path_buf()
    };
    tokio::fs::write(&path, content)
        .await
        .map_err(|err| err.kind())
        .map_err(Error::IO)
        .map(|_| path)
}

在保存文件时,需要

  1. 检查文件的修改状态,如果文件没有修改,不执行保存操作。
  2. 检查文件路径是否为空,如果为空,打开文件选择对话框,否则直接保存文件。
  3. 加入一个新的消息类型SaveButtonPressed,由一个按钮触发,在update函数中执行这个逻辑。
enum Message {
    EditorEdit(text_editor::Action),
    FileOpened(Result<(PathBuf, Arc<String>), Error>),
    FileSaved(Result<PathBuf, Error>),
    NewButtonPressed,
    OpenButtonPressed,
    SaveButtonPressed
}
// ... In `view` function
let controls = row![
    button("New").on_press(Message::NewButtonPressed),
    button("Open").on_press(Message::OpenButtonPressed),
    button("Save").on_press(Message::SaveButtonPressed)
];
// ... In matching logic in `update` function
Message::FileSaved(Ok(path)) => {
    self.path = Some(path);
    self.modified = false;
    Command::none()
},
Message::FileOpened(Err(error)) | Message::FileSaved(Err(error)) => {
    self.error = Some(error);
    Command::none()
},
// ...
Message::SaveButtonPressed => {
    let content = self.content.text();
    match self.modified {
        false => Command::none(),
        true => Command::perform(
            save_file(self.path.clone(), content),
            Message::FileSaved
        )
    }
}

最后,调整三个按钮之间的间距

let controls = row![
    button("New").on_press(Message::NewButtonPressed),
    button("Open").on_press(Message::OpenButtonPressed),
    button("Save").on_press(Message::SaveButtonPressed)
].spacing(10);

以下为完整的main.rs文件内容:

Download source code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use iced::{executor, Application, Command, Element, Length, Settings, Theme};
use iced::widget::{button, column, container, horizontal_space, row, text, text_editor};

fn main() -> iced::Result {
    Editor::run(Settings::default())
}

struct Editor {
    path: Option<PathBuf>,
    content: text_editor::Content,
    modified: bool,
    error: Option<Error>
}

#[derive(Debug, Clone)]
enum Message {
    EditorEdit(text_editor::Action),
    FileOpened(Result<(PathBuf, Arc<String>), Error>),
    FileSaved(Result<PathBuf, Error>),
    NewButtonPressed,
    OpenButtonPressed,
    SaveButtonPressed
}

impl Application for Editor {
    type Message = Message; // Define the type of messages
    type Theme = Theme;
    type Executor = executor::Default; // Engine for running async tasks
    type Flags = ();

    fn new(_flags: Self::Flags) -> (Self, Command<Message>) {
        (
            Self {
                content: text_editor::Content::new(),
                error: None,
                modified: false,
                path: None
            },
            Command::perform(
            load_file(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR"))),
            Message::FileOpened
            )
        )
    }

    fn title(&self) -> String {
        String::from("A text editor")
    }

    fn update(&mut self, message: Message) -> Command<Message> {
        // Handle messages here
        match message {
            Message::EditorEdit(action) => {
                match &action {
                    text_editor::Action::Edit(_) => self.modified = true,
                    _ => {}
                }
                self.content.edit(action);
                Command::none()
            },
            Message::FileOpened(Ok((path, result))) => {
                self.path = Some(path);
                self.modified = false;
                self.content = text_editor::Content::with(&result);
                Command::none()
            },
            Message::FileSaved(Ok(path)) => {
                self.path = Some(path);
                self.modified = false;
                Command::none()
            },
            Message::FileOpened(Err(error)) | Message::FileSaved(Err(error)) => {
                self.error = Some(error);
                Command::none()
            },
            Message::NewButtonPressed => {
                self.content = text_editor::Content::new();
                self.error = None;
                self.path = None;
                self.modified = false;
                Command::none()
            },
            Message::OpenButtonPressed => {
                self.modified = false;
                Command::perform(pick_file(), Message::FileOpened)
            },
            Message::SaveButtonPressed => {
                let content = self.content.text();
                match self.modified {
                    false => Command::none(),
                    true => Command::perform(
                        save_file(self.path.clone(), content),
                        Message::FileSaved
                    )
                }
            }
        }
    }

    fn view(&self) -> Element<'_, Message> {
        // Create the user interface here
        let editor = text_editor(&self.content).on_edit(Message::EditorEdit);
        let controls = row![
            button("New").on_press(Message::NewButtonPressed),
            button("Open").on_press(Message::OpenButtonPressed),
            button("Save").on_press(Message::SaveButtonPressed)
        ].spacing(10);

        // Query cursor position
        let path_indicator = if let Some(error) = &self.error {
            match error {
                Error::DialogClosed => text("Dialog closed"),
                Error::IO(kind) => text(format!("I/O error: {:?}", kind))
            }
        } else {
            let path_text = match &self.path {
                None => String::from("New file"),
                Some(path) => path.to_string_lossy().to_string()
            };
            let suffix = if self.modified { "*" } else { "" };
            text(format!("{}{}", path_text, suffix))
        };
        let cursor_indicator = {
            let (line, column) = self.content.cursor_position();

            text(format!("Line: {}, Column: {}", line + 1, column + 1))
        };
        let status_bar = row![
            path_indicator,
            horizontal_space(Length::Fill),
            cursor_indicator
        ];

        container(column![controls, editor, status_bar].spacing(10)).padding(10).into()
    }

    fn theme(&self) -> Theme {
        Theme::Dark
    }
}

async fn pick_file() -> Result<(PathBuf, Arc<String>), Error> {
    let file_handle = rfd::AsyncFileDialog::new()
        .set_title("Choose a text file...")
        .pick_file()
        .await
        .ok_or(Error::DialogClosed)?;
    load_file(file_handle.path()).await
}

async fn load_file(path: impl AsRef<Path>) -> Result<(PathBuf, Arc<String>), Error> {
    let content = tokio::fs::read_to_string(path.as_ref())
        .await
        .map(Arc::new)
        .map_err(|err| err.kind())
        .map_err(Error::IO);
    content.and_then(|content| Ok((path.as_ref().to_path_buf(), content)))
}

async fn save_file(path: Option<PathBuf>, content: String) -> Result<PathBuf, Error> {
    let path = if let Some(path) = path {
        path
    } else {
        rfd::AsyncFileDialog::new()
            .set_title("Save the file to...")
            .save_file()
            .await
            .ok_or(Error::DialogClosed)?
            .path()
            .to_path_buf()
    };
    tokio::fs::write(&path, content)
        .await
        .map_err(|err| err.kind())
        .map_err(Error::IO)
        .map(|_| path)
}

#[derive(Debug, Clone)]
enum Error {
    DialogClosed,
    IO(io::ErrorKind)
}

评论