editor/
editor.rs

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