editor/
editor.rs

1use fltk::{enums::*, prelude::*, *};
2use std::path::PathBuf;
3use std::sync::LazyLock;
4
5const WIDTH: i32 = 800;
6const HEIGHT: i32 = 600;
7static STATE: LazyLock<app::GlobalState<State>> = LazyLock::new(app::GlobalState::<State>::get);
8
9pub struct State {
10    pub saved: bool,
11    pub buf: text::TextBuffer,
12    pub current_file: PathBuf,
13}
14
15impl State {
16    fn new(buf: text::TextBuffer) -> Self {
17        State {
18            saved: true,
19            buf,
20            current_file: PathBuf::new(),
21        }
22    }
23}
24
25fn init_menu(m: &mut menu::SysMenuBar) {
26    m.add(
27        "&File/&New...\t",
28        Shortcut::Ctrl | 'n',
29        menu::MenuFlag::Normal,
30        menu_cb,
31    );
32    m.add(
33        "&File/&Open...\t",
34        Shortcut::Ctrl | 'o',
35        menu::MenuFlag::Normal,
36        menu_cb,
37    );
38    m.add(
39        "&File/&Save\t",
40        Shortcut::Ctrl | 's',
41        menu::MenuFlag::Normal,
42        menu_cb,
43    );
44    m.add(
45        "&File/Save &as...\t",
46        Shortcut::Ctrl | 'w',
47        menu::MenuFlag::MenuDivider,
48        menu_cb,
49    );
50    let idx = m.add(
51        "&File/&Quit\t",
52        Shortcut::Ctrl | 'q',
53        menu::MenuFlag::Normal,
54        menu_cb,
55    );
56    m.at(idx).unwrap().set_label_color(Color::Red);
57    m.add(
58        "&Edit/Cu&t\t",
59        Shortcut::Ctrl | 'x',
60        menu::MenuFlag::Normal,
61        menu_cb,
62    );
63    m.add(
64        "&Edit/&Copy\t",
65        Shortcut::Ctrl | 'c',
66        menu::MenuFlag::Normal,
67        menu_cb,
68    );
69    m.add(
70        "&Edit/&Paste\t",
71        Shortcut::Ctrl | 'v',
72        menu::MenuFlag::Normal,
73        menu_cb,
74    );
75    m.add(
76        "&Help/&About\t",
77        Shortcut::None,
78        menu::MenuFlag::Normal,
79        menu_cb,
80    );
81}
82
83fn nfc_get_file(mode: dialog::NativeFileChooserType) -> Option<PathBuf> {
84    let mut nfc = dialog::NativeFileChooser::new(mode);
85    if mode == dialog::NativeFileChooserType::BrowseSaveFile {
86        nfc.set_option(dialog::NativeFileChooserOptions::SaveAsConfirm);
87    } else if mode == dialog::NativeFileChooserType::BrowseFile {
88        nfc.set_option(dialog::NativeFileChooserOptions::NoOptions);
89        nfc.set_filter("*.{txt,rs,toml}");
90    }
91    match nfc.show() {
92        Err(e) => {
93            eprintln!("{}", e);
94            None
95        }
96        Ok(a) => match a {
97            dialog::NativeFileChooserAction::Success => {
98                let name = nfc.filename();
99                if name.as_os_str().is_empty() {
100                    dialog::alert("Please specify a file!");
101                    None
102                } else {
103                    Some(name)
104                }
105            }
106            dialog::NativeFileChooserAction::Cancelled => None,
107        },
108    }
109}
110
111fn quit_cb() {
112    STATE.with(|s| {
113        if s.saved {
114            app::quit();
115        } else {
116            let c = dialog::choice(
117                "Are you sure you want to exit without saving?",
118                "&Yes",
119                "&No",
120                "",
121            );
122            if c == Some(0) {
123                app::quit();
124            }
125        }
126    });
127}
128
129fn win_cb(_w: &mut window::Window) {
130    if app::event() == Event::Close {
131        quit_cb();
132    }
133}
134
135fn editor_cb(_e: &mut text::TextEditor) {
136    STATE.with(|s| s.saved = false);
137}
138
139fn handle_drag_drop(editor: &mut text::TextEditor) {
140    editor.handle({
141        let mut dnd = false;
142        let mut released = false;
143        let buf = editor.buffer().unwrap();
144        move |_, ev| match ev {
145            Event::DndEnter => {
146                dnd = true;
147                true
148            }
149            Event::DndDrag => true,
150            Event::DndRelease => {
151                released = true;
152                true
153            }
154            Event::Paste => {
155                if dnd && released {
156                    let path = app::event_text().unwrap();
157                    let path = path.trim();
158                    let path = path.replace("file://", "");
159                    let path = std::path::PathBuf::from(&path);
160                    if path.exists() {
161                        // we use a timeout to avoid pasting the path into the buffer
162                        app::add_timeout(0.0, {
163                            let mut buf = buf.clone();
164                            move |_| match buf.load_file(&path) {
165                                Ok(_) => (),
166                                Err(e) => dialog::alert(&format!(
167                                    "An issue occured while loading the file: {e}"
168                                )),
169                            }
170                        });
171                    }
172                    dnd = false;
173                    released = false;
174                    true
175                } else {
176                    false
177                }
178            }
179            Event::DndLeave => {
180                dnd = false;
181                released = false;
182                true
183            }
184            _ => false,
185        }
186    });
187}
188
189fn menu_cb(m: &mut impl MenuExt) {
190    if let Ok(mpath) = m.item_pathname(None) {
191        let ed: text::TextEditor = app::widget_from_id("ed").unwrap();
192        match mpath.as_str() {
193            "&File/&New...\t" => {
194                STATE.with(|s| {
195                    if !s.buf.text().is_empty() {
196                        let c = dialog::choice(
197                            "Are you sure you want to clear the buffer?",
198                            "&Yes",
199                            "&No",
200                            "",
201                        );
202                        if c == Some(0) {
203                            s.buf.set_text("");
204                            s.saved = false;
205                        }
206                    }
207                });
208            }
209            "&File/&Open...\t" => {
210                if let Some(c) = nfc_get_file(dialog::NativeFileChooserType::BrowseFile) {
211                    if let Ok(text) = std::fs::read_to_string(&c) {
212                        STATE.with(move |s| {
213                            s.buf.set_text(&text);
214                            s.saved = false;
215                            s.current_file = c.clone();
216                        });
217                    }
218                }
219            }
220            "&File/&Save\t" => {
221                STATE.with(|s| {
222                    if !s.saved && s.current_file.exists() {
223                        std::fs::write(&s.current_file, s.buf.text()).ok();
224                    }
225                });
226            }
227            "&File/Save &as...\t" => {
228                if let Some(c) = nfc_get_file(dialog::NativeFileChooserType::BrowseSaveFile) {
229                    STATE.with(move |s| {
230                        std::fs::write(&c, s.buf.text()).ok();
231                        s.saved = true;
232                        s.current_file = c.clone();
233                    });
234                }
235            }
236            "&File/&Quit\t" => quit_cb(),
237            "&Edit/Cu&t\t" => ed.cut(),
238            "&Edit/&Copy\t" => ed.copy(),
239            "&Edit/&Paste\t" => ed.paste(),
240            "&Help/&About\t" => dialog::message("A minimal text editor written using fltk-rs!"),
241            _ => unreachable!(),
242        }
243    }
244}
245
246fn main() {
247    let a = app::App::default().with_scheme(app::Scheme::Oxy);
248    app::get_system_colors();
249
250    let mut buf = text::TextBuffer::default();
251    buf.set_tab_distance(4);
252
253    let state = State::new(buf.clone());
254    app::GlobalState::new(state);
255
256    let mut w = window::Window::default()
257        .with_size(WIDTH, HEIGHT)
258        .with_label("Ted");
259    w.set_xclass("ted");
260    {
261        let mut col = group::Flex::default_fill().column();
262        col.set_pad(0);
263        let mut m = menu::SysMenuBar::default();
264        init_menu(&mut m);
265        let mut ed = text::TextEditor::default().with_id("ed");
266        ed.set_buffer(buf);
267        ed.set_linenumber_width(40);
268        ed.set_text_font(Font::Courier);
269        ed.set_when(When::Changed);
270        ed.set_callback(editor_cb);
271        handle_drag_drop(&mut ed);
272        w.resizable(&col);
273        col.fixed(&m, 30);
274        col.end();
275    }
276    w.end();
277    w.show();
278    w.set_callback(win_cb);
279    a.run().unwrap();
280}