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