editor2/
editor2.rs

1use fltk::{
2    app, dialog,
3    enums::{CallbackTrigger, Color, Event, Font, FrameType, Shortcut},
4    menu,
5    prelude::*,
6    printer, text, window,
7};
8use std::path::PathBuf;
9use std::{
10    error,
11    ops::{Deref, DerefMut},
12    path,
13};
14
15#[derive(Copy, Clone)]
16pub enum Message {
17    Changed,
18    New,
19    Open,
20    Save,
21    SaveAs,
22    Print,
23    Quit,
24    Cut,
25    Copy,
26    Paste,
27    About,
28}
29
30pub fn center() -> (i32, i32) {
31    (
32        (app::screen_size().0 / 2.0) as i32,
33        (app::screen_size().1 / 2.0) as i32,
34    )
35}
36
37fn nfc_get_file(mode: dialog::NativeFileChooserType) -> Option<PathBuf> {
38    let mut nfc = dialog::NativeFileChooser::new(mode);
39    if mode == dialog::NativeFileChooserType::BrowseSaveFile {
40        nfc.set_option(dialog::NativeFileChooserOptions::SaveAsConfirm);
41    } else if mode == dialog::NativeFileChooserType::BrowseFile {
42        nfc.set_option(dialog::NativeFileChooserOptions::NoOptions);
43        nfc.set_filter("*.{txt,rs,toml}");
44    }
45    match nfc.try_show() {
46        Err(e) => {
47            eprintln!("{}", e);
48            None
49        }
50        Ok(a) => match a {
51            dialog::NativeFileChooserAction::Success => {
52                let name = nfc.filename();
53                if name.as_os_str().is_empty() {
54                    dialog::alert(center().0 - 200, center().1 - 100, "Please specify a file!");
55                    None
56                } else {
57                    Some(name)
58                }
59            }
60            dialog::NativeFileChooserAction::Cancelled => None,
61        },
62    }
63}
64
65pub struct MyEditor {
66    editor: text::TextEditor,
67}
68
69impl MyEditor {
70    pub fn new(buf: text::TextBuffer) -> Self {
71        let mut editor = text::TextEditor::new(5, 35, 790, 560, "");
72        editor.set_buffer(Some(buf));
73
74        #[cfg(target_os = "macos")]
75        editor.resize(5, 5, 790, 590);
76
77        editor.set_scrollbar_size(15);
78        editor.set_text_font(Font::Courier);
79        editor.set_linenumber_width(32);
80        editor.set_linenumber_fgcolor(Color::from_u32(0x008b_8386));
81        editor.set_trigger(CallbackTrigger::Changed);
82
83        Self { editor }
84    }
85}
86
87impl Deref for MyEditor {
88    type Target = text::TextEditor;
89
90    fn deref(&self) -> &Self::Target {
91        &self.editor
92    }
93}
94
95impl DerefMut for MyEditor {
96    fn deref_mut(&mut self) -> &mut Self::Target {
97        &mut self.editor
98    }
99}
100
101pub struct MyMenu {
102    menu: menu::SysMenuBar,
103}
104
105impl MyMenu {
106    pub fn new(s: &app::Sender<Message>) -> Self {
107        let mut menu = menu::SysMenuBar::default().with_size(800, 35);
108        menu.set_frame(FrameType::FlatBox);
109        menu.add_emit(
110            "&File/&New...\t",
111            Shortcut::Ctrl | 'n',
112            menu::MenuFlag::Normal,
113            *s,
114            Message::New,
115        );
116
117        menu.add_emit(
118            "&File/&Open...\t",
119            Shortcut::Ctrl | 'o',
120            menu::MenuFlag::Normal,
121            *s,
122            Message::Open,
123        );
124
125        menu.add_emit(
126            "&File/&Save\t",
127            Shortcut::Ctrl | 's',
128            menu::MenuFlag::Normal,
129            *s,
130            Message::Save,
131        );
132
133        menu.add_emit(
134            "&File/Save &as...\t",
135            Shortcut::Ctrl | 'w',
136            menu::MenuFlag::Normal,
137            *s,
138            Message::SaveAs,
139        );
140
141        menu.add_emit(
142            "&File/&Print...\t",
143            Shortcut::Ctrl | 'p',
144            menu::MenuFlag::MenuDivider,
145            *s,
146            Message::Print,
147        );
148
149        menu.add_emit(
150            "&File/&Quit\t",
151            Shortcut::Ctrl | 'q',
152            menu::MenuFlag::Normal,
153            *s,
154            Message::Quit,
155        );
156
157        menu.add_emit(
158            "&Edit/Cu&t\t",
159            Shortcut::Ctrl | 'x',
160            menu::MenuFlag::Normal,
161            *s,
162            Message::Cut,
163        );
164
165        menu.add_emit(
166            "&Edit/&Copy\t",
167            Shortcut::Ctrl | 'c',
168            menu::MenuFlag::Normal,
169            *s,
170            Message::Copy,
171        );
172
173        menu.add_emit(
174            "&Edit/&Paste\t",
175            Shortcut::Ctrl | 'v',
176            menu::MenuFlag::Normal,
177            *s,
178            Message::Paste,
179        );
180
181        menu.add_emit(
182            "&Help/&About\t",
183            Shortcut::None,
184            menu::MenuFlag::Normal,
185            *s,
186            Message::About,
187        );
188
189        Self { menu }
190    }
191}
192
193pub struct MyApp {
194    app: app::App,
195    modified: bool,
196    filename: Option<PathBuf>,
197    r: app::Receiver<Message>,
198    main_win: window::Window,
199    menu: MyMenu,
200    buf: text::TextBuffer,
201    editor: MyEditor,
202    printable: text::TextDisplay,
203}
204
205impl MyApp {
206    pub fn new(args: Vec<String>) -> Self {
207        let app = app::App::default().with_scheme(app::Scheme::Gtk);
208        app::background(211, 211, 211);
209        let (s, r) = app::channel::<Message>();
210        let mut buf = text::TextBuffer::default();
211        buf.set_tab_distance(4);
212        let mut main_win = window::Window::default()
213            .with_size(800, 600)
214            .center_screen()
215            .with_label("RustyEd");
216        let menu = MyMenu::new(&s);
217        let modified = false;
218        menu.menu.find_item("&File/&Save\t").unwrap().deactivate();
219        let mut editor = MyEditor::new(buf.clone());
220        editor.emit(s, Message::Changed);
221        main_win.make_resizable(true);
222        // only resize editor, not the menu bar
223        main_win.resizable(&*editor);
224        main_win.end();
225        main_win.show();
226        main_win.set_callback(move |_| {
227            if app::event() == Event::Close {
228                s.send(Message::Quit);
229            }
230        });
231        let filename = if args.len() > 1 {
232            let file = path::Path::new(&args[1]);
233            assert!(
234                file.exists() && file.is_file(),
235                "An error occurred while opening the file!"
236            );
237            match buf.load_file(&args[1]) {
238                Ok(_) => Some(PathBuf::from(args[1].clone())),
239                Err(e) => {
240                    dialog::alert(
241                        center().0 - 200,
242                        center().1 - 100,
243                        &format!("An issue occured while loading the file: {e}"),
244                    );
245                    None
246                }
247            }
248        } else {
249            None
250        };
251
252        // Handle drag and drop
253        editor.handle({
254            let mut dnd = false;
255            let mut released = false;
256            let buf = buf.clone();
257            move |_, ev| match ev {
258                Event::DndEnter => {
259                    dnd = true;
260                    true
261                }
262                Event::DndDrag => true,
263                Event::DndRelease => {
264                    released = true;
265                    true
266                }
267                Event::Paste => {
268                    if dnd && released {
269                        let path = app::event_text();
270                        let path = path.trim();
271                        let path = path.replace("file://", "");
272                        let path = std::path::PathBuf::from(&path);
273                        if path.exists() {
274                            // we use a timeout to avoid pasting the path into the buffer
275                            app::add_timeout3(0.0, {
276                                let mut buf = buf.clone();
277                                move |_| match buf.load_file(&path) {
278                                    Ok(_) => (),
279                                    Err(e) => dialog::alert(
280                                        center().0 - 200,
281                                        center().1 - 100,
282                                        &format!("An issue occured while loading the file: {e}"),
283                                    ),
284                                }
285                            });
286                        }
287                        dnd = false;
288                        released = false;
289                        true
290                    } else {
291                        false
292                    }
293                }
294                Event::DndLeave => {
295                    dnd = false;
296                    released = false;
297                    true
298                }
299                _ => false,
300            }
301        });
302
303        // What shows when we attempt to print
304        let mut printable = text::TextDisplay::default();
305        printable.set_frame(FrameType::NoBox);
306        printable.set_scrollbar_size(0);
307        printable.set_buffer(Some(buf.clone()));
308
309        Self {
310            app,
311            modified,
312            filename,
313            r,
314            main_win,
315            menu,
316            buf,
317            editor,
318            printable,
319        }
320    }
321
322    /** Called by "Save", test if file can be written, otherwise call save_file_as()
323     * afterwards. Will return true if the file is succesfully saved. */
324    pub fn save_file(&mut self) -> Result<bool, Box<dyn error::Error>> {
325        match &self.filename {
326            Some(f) => {
327                self.buf.save_file(f)?;
328                self.modified = false;
329                self.menu
330                    .menu
331                    .find_item("&File/&Save\t")
332                    .unwrap()
333                    .deactivate();
334                self.menu
335                    .menu
336                    .find_item("&File/&Quit\t")
337                    .unwrap()
338                    .set_label_color(Color::Black);
339                let name = match &self.filename {
340                    Some(f) => f.to_string_lossy().to_string(),
341                    None => "(Untitled)".to_string(),
342                };
343                self.main_win.set_label(&format!("{name} - RustyEd"));
344                Ok(true)
345            }
346            None => self.save_file_as(),
347        }
348    }
349
350    /** Called by "Save As..." or by "Save" in case no file was set yet.
351     * Returns true if the file was succesfully saved. */
352    pub fn save_file_as(&mut self) -> Result<bool, Box<dyn error::Error>> {
353        if let Some(c) = nfc_get_file(dialog::NativeFileChooserType::BrowseSaveFile) {
354            self.buf.save_file(&c)?;
355            self.modified = false;
356            self.menu
357                .menu
358                .find_item("&File/&Save\t")
359                .unwrap()
360                .deactivate();
361            self.menu
362                .menu
363                .find_item("&File/&Quit\t")
364                .unwrap()
365                .set_label_color(Color::Black);
366            self.filename = Some(c);
367            self.main_win
368                .set_label(&format!("{:?} - RustyEd", self.filename.as_ref().unwrap()));
369            Ok(true)
370        } else {
371            Ok(false)
372        }
373    }
374
375    pub fn launch(&mut self) {
376        while self.app.wait() {
377            use Message::*;
378            if let Some(msg) = self.r.recv() {
379                match msg {
380                    Changed => {
381                        if !self.modified {
382                            self.modified = true;
383                            self.menu.menu.find_item("&File/&Save\t").unwrap().activate();
384                            self.menu.menu.find_item("&File/&Quit\t").unwrap().set_label_color(Color::Red);
385                            let name = match &self.filename {
386                                Some(f) => f.to_string_lossy().to_string(),
387                                None => "(Untitled)".to_string(),
388                            };
389                            self.main_win.set_label(&format!("* {name} - RustyEd"));
390                        }
391                    }
392                    New => {
393                        if self.buf.text() != "" {
394                            let clear = if let Some(x) = dialog::choice2(center().0 - 200, center().1 - 100, "File unsaved, Do you wish to continue?", "&Yes", "&No!", "") {
395                                x == 0
396                            } else {
397                                false
398                            };
399                            if clear {
400                                self.buf.set_text("");
401                            }
402                        }
403                    },
404                    Open => {
405                        if let Some(c) = nfc_get_file(dialog::NativeFileChooserType::BrowseFile) {
406                            if c.exists() {
407                                match self.buf.load_file(&c) {
408                                    Ok(_) => self.filename = Some(c),
409                                    Err(e) => dialog::alert(center().0 - 200, center().1 - 100, &format!("An issue occured while loading the file: {e}")),
410                                }
411                            } else {
412                                dialog::alert(center().0 - 200, center().1 - 100, "File does not exist!")
413                            }
414                        }
415                    },
416                    Save => { self.save_file().unwrap(); },
417                    SaveAs => { self.save_file_as().unwrap(); },
418                    Print => {
419                        let mut printer = printer::Printer::default();
420                        if printer.begin_job(0).is_ok() {
421                            let (w, h) = printer.printable_rect();
422                            self.printable.set_size(w - 40, h - 40);
423                            // Needs cleanup
424                            let line_count = self.printable.count_lines(0, self.printable.buffer().unwrap().length(), true) / 45;
425                            for i in 0..=line_count {
426                                self.printable.scroll(45 * i, 0);
427                                printer.begin_page().ok();
428                                printer.print_widget(&self.printable, 20, 20);
429                                printer.end_page().ok();
430                            }
431                            printer.end_job();
432                        }
433                    },
434                    Quit => {
435                        if self.modified {
436                            match dialog::choice2(center().0 - 200, center().1 - 100,
437                                "Would you like to save your work?", "&Yes", "&No", "") {
438                                Some(0) => {
439                                    if self.save_file().unwrap() {
440                                        self.app.quit();
441                                    }
442                                },
443                                Some(1) => { self.app.quit() },
444                                Some(_) | None  => (),
445                            }
446                        } else {
447                            self.app.quit();
448                        }
449                    },
450                    Cut => self.editor.cut(),
451                    Copy => self.editor.copy(),
452                    Paste => self.editor.paste(),
453                    About => dialog::message(center().0 - 300, center().1 - 100, "This is an example application written in Rust and using the FLTK Gui library."),
454                }
455            }
456        }
457    }
458}
459
460fn main() {
461    let args: Vec<_> = std::env::args().collect();
462    let mut app = MyApp::new(args);
463    app.launch();
464}