editor2/
editor2.rs

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