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 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 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 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 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 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 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 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}