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