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