fmt

f285279 · gmorell · 2025-07-25 16:39

2 files +585 -450

Files changed

modified src/main.rs
+585 −450
@@ -1,14 +1,14 @@
1 +use comrak::{markdown_to_html, ComrakOptions};
1 2 use eframe::{egui, epaint::Color32, App, CreationContext};
2 −use std::path::PathBuf;
3 −use std::time::{Duration, Instant};
4 −use std::fs;
5 3 use egui::ScrollArea;
4 +use egui_material_icons::icons::*;
6 5 use rfd::FileDialog;
7 −use comrak::{markdown_to_html, ComrakOptions};
8 6 use serde::{Deserialize, Serialize};
7 +use std::fs;
8 +use std::path::PathBuf;
9 9 use std::sync::mpsc::{channel, Receiver, Sender};
10 10 use std::thread;
11 −use egui_material_icons::icons::*;
11 +use std::time::{Duration, Instant};
12 12
13 13 // Theme configuration
14 14 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -25,16 +25,16 @@ struct MarkPrompter {
25 25 current_file: Option<PathBuf>,
26 26 content: String,
27 27 parsed_content: String,
28 −
28 +
29 29 // Scroll control
30 30 scroll_position: f32,
31 − scroll_speed: f32, // pixels per second
31 + scroll_speed: f32, // pixels per second
32 32 is_playing: bool,
33 33 last_update: Instant,
34 −
34 +
35 35 // Display settings
36 36 font_size: f32,
37 −
37 +
38 38 // Feature toggles
39 39 pause_at_headings: bool,
40 40 auto_restart: bool,
@@ -42,11 +42,11 @@ struct MarkPrompter {
42 42 current_heading_pause: Option<f32>,
43 43 heading_line_indices: Vec<usize>,
44 44 last_checked_heading_idx: usize,
45 −
45 +
46 46 // Theme
47 47 current_theme: Theme,
48 48 available_themes: Vec<Theme>,
49 −
49 +
50 50 // File watcher
51 51 _file_watcher_tx: Option<Sender<()>>,
52 52 file_watcher_rx: Option<Receiver<()>>,
@@ -99,7 +99,7 @@ impl MarkPrompter {
99 99 fn new(cc: &CreationContext) -> Self {
100 100 // Initialize material icons
101 101 egui_material_icons::initialize(&cc.egui_ctx);
102 −
102 +
103 103 // Configure fonts
104 104 let mut style = (*cc.egui_ctx.style()).clone();
105 105 style.text_styles = [
@@ -108,22 +108,26 @@ impl MarkPrompter {
108 108 (egui::TextStyle::Monospace, egui::FontId::monospace(14.0)),
109 109 (egui::TextStyle::Button, egui::FontId::proportional(16.0)),
110 110 (egui::TextStyle::Small, egui::FontId::proportional(10.0)),
111 − ].into();
111 + ]
112 + .into();
112 113 cc.egui_ctx.set_style(style);
113 −
114 +
114 115 // Load themes from config file if it exists
115 116 let mut app = Self::default();
116 117 match load_themes_and_preference() {
117 118 Ok((themes, saved_theme)) => {
118 119 println!("Themes loaded successfully: {} themes", themes.len());
119 120 app.available_themes = themes;
120 −
121 +
121 122 // Load saved theme preference
122 123 if let Some(saved_theme_name) = saved_theme {
123 124 // Try to find and set the saved theme
124 − if let Some(saved_theme) = app.available_themes.iter()
125 + if let Some(saved_theme) = app
126 + .available_themes
127 + .iter()
125 128 .find(|t| t.name == saved_theme_name.trim())
126 − .cloned() {
129 + .cloned()
130 + {
127 131 println!("Restored saved theme: {}", saved_theme.name);
128 132 app.current_theme = saved_theme;
129 133 } else if !app.available_themes.is_empty() {
@@ -139,18 +143,24 @@ impl MarkPrompter {
139 143 println!("Error loading themes: {}", e);
140 144 }
141 145 }
142 −
146 +
143 147 app
144 148 }
145 −
149 +
146 150 // Parse and render inline markdown formatting
147 − fn render_formatted_text(&self, ui: &mut egui::Ui, text: &str, base_color: Color32, base_size: f32) {
148 − use egui::{FontId, TextFormat, text::LayoutJob};
149 −
151 + fn render_formatted_text(
152 + &self,
153 + ui: &mut egui::Ui,
154 + text: &str,
155 + base_color: Color32,
156 + base_size: f32,
157 + ) {
158 + use egui::{text::LayoutJob, FontId, TextFormat};
159 +
150 160 let mut job = LayoutJob::default();
151 161 let mut chars = text.chars().peekable();
152 162 let mut current_text = String::new();
153 −
163 +
154 164 while let Some(ch) = chars.next() {
155 165 match ch {
156 166 '*' | '_' => {
@@ -159,21 +169,25 @@ impl MarkPrompter {
159 169 if next_ch == ch {
160 170 // Double marker - bold
161 171 chars.next(); // consume second marker
162 −
172 +
163 173 // Add any pending text
164 174 if !current_text.is_empty() {
165 − job.append(&current_text, 0.0, TextFormat {
166 − font_id: FontId::proportional(base_size),
167 − color: base_color,
168 − ..Default::default()
169 − });
175 + job.append(
176 + &current_text,
177 + 0.0,
178 + TextFormat {
179 + font_id: FontId::proportional(base_size),
180 + color: base_color,
181 + ..Default::default()
182 + },
183 + );
170 184 current_text.clear();
171 185 }
172 −
186 +
173 187 // Find closing markers
174 188 let mut content = String::new();
175 189 let mut found_closing = false;
176 −
190 +
177 191 while let Some(inner_ch) = chars.next() {
178 192 if inner_ch == ch {
179 193 if let Some(&next_inner) = chars.peek() {
@@ -186,14 +200,18 @@ impl MarkPrompter {
186 200 }
187 201 content.push(inner_ch);
188 202 }
189 −
203 +
190 204 if found_closing {
191 205 // Bold text - use larger size to simulate bold
192 − job.append(&content, 0.0, TextFormat {
193 − font_id: FontId::proportional(base_size * 1.15),
194 − color: base_color,
195 − ..Default::default()
196 − });
206 + job.append(
207 + &content,
208 + 0.0,
209 + TextFormat {
210 + font_id: FontId::proportional(base_size * 1.15),
211 + color: base_color,
212 + ..Default::default()
213 + },
214 + );
197 215 } else {
198 216 // No closing found, treat as normal text
199 217 current_text.push(ch);
@@ -204,18 +222,22 @@ impl MarkPrompter {
204 222 // Single marker - italic
205 223 // Add any pending text
206 224 if !current_text.is_empty() {
207 − job.append(&current_text, 0.0, TextFormat {
208 − font_id: FontId::proportional(base_size),
209 − color: base_color,
210 − ..Default::default()
211 − });
225 + job.append(
226 + &current_text,
227 + 0.0,
228 + TextFormat {
229 + font_id: FontId::proportional(base_size),
230 + color: base_color,
231 + ..Default::default()
232 + },
233 + );
212 234 current_text.clear();
213 235 }
214 −
236 +
215 237 // Find closing marker
216 238 let mut content = String::new();
217 239 let mut found_closing = false;
218 −
240 +
219 241 while let Some(inner_ch) = chars.next() {
220 242 if inner_ch == ch {
221 243 found_closing = true;
@@ -223,19 +245,23 @@ impl MarkPrompter {
223 245 }
224 246 content.push(inner_ch);
225 247 }
226 −
248 +
227 249 if found_closing {
228 250 // Italic text - use slightly smaller and different color
229 − job.append(&content, 0.0, TextFormat {
230 − font_id: FontId::proportional(base_size * 0.95),
231 − color: Color32::from_rgb(
232 − (base_color.r() as f32 * 0.9) as u8,
233 − (base_color.g() as f32 * 0.9) as u8,
234 − (base_color.b() as f32 * 0.9) as u8,
235 − ),
236 − italics: true,
237 − ..Default::default()
238 − });
251 + job.append(
252 + &content,
253 + 0.0,
254 + TextFormat {
255 + font_id: FontId::proportional(base_size * 0.95),
256 + color: Color32::from_rgb(
257 + (base_color.r() as f32 * 0.9) as u8,
258 + (base_color.g() as f32 * 0.9) as u8,
259 + (base_color.b() as f32 * 0.9) as u8,
260 + ),
261 + italics: true,
262 + ..Default::default()
263 + },
264 + );
239 265 } else {
240 266 // No closing found, treat as normal text
241 267 current_text.push(ch);
@@ -249,17 +275,21 @@ impl MarkPrompter {
249 275 '`' => {
250 276 // Code formatting
251 277 if !current_text.is_empty() {
252 − job.append(&current_text, 0.0, TextFormat {
253 − font_id: FontId::proportional(base_size),
254 − color: base_color,
255 − ..Default::default()
256 − });
278 + job.append(
279 + &current_text,
280 + 0.0,
281 + TextFormat {
282 + font_id: FontId::proportional(base_size),
283 + color: base_color,
284 + ..Default::default()
285 + },
286 + );
257 287 current_text.clear();
258 288 }
259 −
289 +
260 290 let mut content = String::new();
261 291 let mut found_closing = false;
262 −
292 +
263 293 while let Some(inner_ch) = chars.next() {
264 294 if inner_ch == '`' {
265 295 found_closing = true;
@@ -267,15 +297,19 @@ impl MarkPrompter {
267 297 }
268 298 content.push(inner_ch);
269 299 }
270 −
300 +
271 301 if found_closing {
272 302 // Code text with background
273 − job.append(&content, 0.0, TextFormat {
274 − font_id: FontId::monospace(base_size * 0.9),
275 − color: base_color,
276 − background: Color32::from_rgba_premultiplied(80, 80, 80, 40),
277 − ..Default::default()
278 − });
303 + job.append(
304 + &content,
305 + 0.0,
306 + TextFormat {
307 + font_id: FontId::monospace(base_size * 0.9),
308 + color: base_color,
309 + background: Color32::from_rgba_premultiplied(80, 80, 80, 40),
310 + ..Default::default()
311 + },
312 + );
279 313 } else {
280 314 current_text.push('`');
281 315 current_text.push_str(&content);
@@ -286,28 +320,32 @@ impl MarkPrompter {
286 320 }
287 321 }
288 322 }
289 −
323 +
290 324 // Add any remaining text
291 325 if !current_text.is_empty() {
292 − job.append(&current_text, 0.0, TextFormat {
293 − font_id: FontId::proportional(base_size),
294 − color: base_color,
295 − ..Default::default()
296 − });
326 + job.append(
327 + &current_text,
328 + 0.0,
329 + TextFormat {
330 + font_id: FontId::proportional(base_size),
331 + color: base_color,
332 + ..Default::default()
333 + },
334 + );
297 335 }
298 −
336 +
299 337 ui.label(job);
300 338 }
301 −
339 +
302 340 fn open_file(&mut self) {
303 341 if let Some(path) = FileDialog::new()
304 342 .add_filter("Markdown", &["md", "markdown"])
305 − .pick_file()
343 + .pick_file()
306 344 {
307 345 self.load_file(path);
308 346 }
309 347 }
310 −
348 +
311 349 fn load_file(&mut self, path: PathBuf) {
312 350 match fs::read_to_string(&path) {
313 351 Ok(content) => {
@@ -316,19 +354,22 @@ impl MarkPrompter {
316 354 self.current_file = Some(path.clone());
317 355 self.scroll_position = 0.0;
318 356 self.last_checked_heading_idx = 0;
319 −
357 +
320 358 // Set up file watcher
321 359 let (tx, rx) = channel();
322 360 self.file_watcher_rx = Some(rx);
323 −
361 +
324 362 let path_clone = path.clone();
325 363 let watcher_tx = tx.clone();
326 364 thread::spawn(move || {
327 − let mut last_modified = fs::metadata(&path_clone).ok().map(|m| m.modified().ok()).flatten();
328 −
365 + let mut last_modified = fs::metadata(&path_clone)
366 + .ok()
367 + .map(|m| m.modified().ok())
368 + .flatten();
369 +
329 370 loop {
330 371 thread::sleep(Duration::from_secs(1));
331 −
372 +
332 373 if let Ok(metadata) = fs::metadata(&path_clone) {
333 374 if let Ok(modified) = metadata.modified() {
334 375 if let Some(last) = last_modified {
@@ -343,47 +384,51 @@ impl MarkPrompter {
343 384 }
344 385 }
345 386 });
346 −
387 +
347 388 self._file_watcher_tx = Some(tx);
348 − },
389 + }
349 390 Err(e) => {
350 391 eprintln!("Error loading file: {}", e);
351 392 }
352 393 }
353 394 }
354 −
395 +
355 396 fn parse_markdown(&mut self) {
356 397 let mut options = ComrakOptions::default();
357 398 options.extension.strikethrough = true;
358 399 options.extension.table = true;
359 400 options.extension.tasklist = true;
360 401 options.extension.footnotes = true;
361 −
402 +
362 403 self.parsed_content = markdown_to_html(&self.content, &options);
363 −
404 +
364 405 // Extract heading positions for pause-at-headings feature if enabled
365 406 if self.pause_at_headings {
366 407 self.extract_heading_positions();
367 408 }
368 409 }
369 −
410 +
370 411 fn extract_heading_positions(&mut self) {
371 412 // Simple approach: check each line for markdown heading markers
372 413 let lines: Vec<&str> = self.content.lines().collect();
373 414 let mut heading_line_indices = Vec::new();
374 −
415 +
375 416 for (i, line) in lines.iter().enumerate() {
376 − if line.starts_with("# ") || line.starts_with("## ") ||
377 − line.starts_with("### ") || line.starts_with("#### ") ||
378 − line.starts_with("##### ") || line.starts_with("###### ") {
417 + if line.starts_with("# ")
418 + || line.starts_with("## ")
419 + || line.starts_with("### ")
420 + || line.starts_with("#### ")
421 + || line.starts_with("##### ")
422 + || line.starts_with("###### ")
423 + {
379 424 heading_line_indices.push(i);
380 425 }
381 426 }
382 −
427 +
383 428 // We'll use this information in the update_scroll method
384 429 self.heading_line_indices = heading_line_indices;
385 430 }
386 −
431 +
387 432 fn check_file_updates(&mut self) {
388 433 if let Some(rx) = &self.file_watcher_rx {
389 434 if rx.try_recv().is_ok() {
@@ -396,12 +441,12 @@ impl MarkPrompter {
396 441 }
397 442 }
398 443 }
399 −
444 +
400 445 fn update_scroll(&mut self, dt: f32) {
401 446 if !self.is_playing {
402 447 return;
403 448 }
404 −
449 +
405 450 // Handle heading pause if enabled
406 451 if let Some(remaining) = self.current_heading_pause {
407 452 if remaining > 0.0 {
@@ -411,17 +456,22 @@ impl MarkPrompter {
411 456 self.current_heading_pause = None;
412 457 }
413 458 }
414 −
459 +
415 460 // Calculate new scroll position
416 461 self.scroll_position += self.scroll_speed * dt;
417 −
462 +
418 463 // Check if we should pause at a heading
419 464 if self.pause_at_headings && !self.heading_line_indices.is_empty() {
420 465 // Calculate approximate line based on scroll position and font size
421 466 let approximate_line = (self.scroll_position / (self.font_size * 1.5)) as usize;
422 −
467 +
423 468 // Check if we're approaching a heading
424 − for (idx, &heading_line) in self.heading_line_indices.iter().enumerate().skip(self.last_checked_heading_idx) {
469 + for (idx, &heading_line) in self
470 + .heading_line_indices
471 + .iter()
472 + .enumerate()
473 + .skip(self.last_checked_heading_idx)
474 + {
425 475 // If we've scrolled past this heading
426 476 if approximate_line >= heading_line && idx >= self.last_checked_heading_idx {
427 477 // Pause scrolling for the specified duration
@@ -437,19 +487,19 @@ impl MarkPrompter {
437 487 impl App for MarkPrompter {
438 488 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
439 489 self.check_file_updates();
440 −
490 +
441 491 // Set background color from theme
442 492 let bg_color = Color32::from_rgb(
443 493 self.current_theme.background_color[0],
444 494 self.current_theme.background_color[1],
445 − self.current_theme.background_color[2]
495 + self.current_theme.background_color[2],
446 496 );
447 −
497 +
448 498 let mut style = (*ctx.style()).clone();
449 499 style.visuals.panel_fill = bg_color;
450 500 style.visuals.window_fill = bg_color;
451 501 ctx.set_style(style);
452 −
502 +
453 503 egui::CentralPanel::default().show(ctx, |ui| {
454 504 // Use columns with custom width ratio - give more space to controls panel
455 505 ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
@@ -460,238 +510,322 @@ impl App for MarkPrompter {
460 510 // Left column - Controls panel
461 511 ui.heading("MP");
462 512 ui.add_space(10.0);
463 −
464 − // File controls
465 − if ui.add_sized([80.0, 80.0], egui::Button::new(egui::RichText::new(format!("{} ", ICON_FOLDER_OPEN)).size(28.0))).clicked() {
466 − self.open_file();
467 − }
468 −
469 − ui.add_space(20.0);
470 − ui.separator();
471 − ui.add_space(10.0);
472 −
473 − // Playback controls
474 − ui.heading("Playback");
475 − ui.add_space(5.0);
476 −
477 − ui.horizontal(|ui| {
478 − let play_pause_text = if self.is_playing {
479 − egui::RichText::new(ICON_PAUSE).size(48.0)
480 − } else {
481 − egui::RichText::new(ICON_PLAY_ARROW).size(48.0)
482 − };
483 −
484 − if ui.add_sized([80.0, 80.0], egui::Button::new(play_pause_text)).clicked() {
485 − self.is_playing = !self.is_playing;
486 − self.last_update = Instant::now();
487 − }
488 −
489 − if ui.add_sized([80.0, 80.0], egui::Button::new(egui::RichText::new(ICON_SKIP_PREVIOUS).size(48.0))).clicked() {
490 − self.scroll_position = 0.0;
491 − self.last_checked_heading_idx = 0;
492 − }
493 − });
494 −
495 − ui.add_space(10.0);
496 −
497 − // Speed controls
498 − ui.label("Scroll Speed");
499 − ui.horizontal(|ui| {
500 − if ui.add_sized([60.0, 60.0], egui::Button::new(egui::RichText::new(ICON_REMOVE).size(36.0))).clicked() {
501 − self.scroll_speed = (self.scroll_speed - 10.0).max(10.0);
513 +
514 + // File controls
515 + if ui
516 + .add_sized(
517 + [80.0, 80.0],
518 + egui::Button::new(
519 + egui::RichText::new(format!("{} ", ICON_FOLDER_OPEN))
520 + .size(28.0),
521 + ),
522 + )
523 + .clicked()
524 + {
525 + self.open_file();
502 526 }
527 +
528 + ui.add_space(20.0);
529 + ui.separator();
503 530 ui.add_space(10.0);
504 − ui.label(egui::RichText::new(format!("{}px/s", self.scroll_speed as i32)).size(20.0));
505 − ui.add_space(10.0);
506 − if ui.add_sized([60.0, 60.0], egui::Button::new(egui::RichText::new(ICON_ADD).size(36.0))).clicked() {
507 − self.scroll_speed = (self.scroll_speed + 10.0).min(500.0);
508 − }
509 − });
510 −
511 − ui.add_space(20.0);
512 − ui.separator();
513 − ui.add_space(10.0);
514 −
515 − // Feature toggles
516 − ui.heading("Settings");
517 − ui.add_space(5.0);
518 −
519 − ui.checkbox(&mut self.pause_at_headings, "Pause at Headings");
520 −
521 − if self.pause_at_headings {
531 +
532 + // Playback controls
533 + ui.heading("Playback");
534 + ui.add_space(5.0);
535 +
522 536 ui.horizontal(|ui| {
523 − ui.label("Duration:");
524 − ui.add(egui::Slider::new(&mut self.heading_pause_duration, 0.5..=10.0)
525 − .suffix("s")
526 − .text("sec"));
537 + let play_pause_text = if self.is_playing {
538 + egui::RichText::new(ICON_PAUSE).size(48.0)
539 + } else {
540 + egui::RichText::new(ICON_PLAY_ARROW).size(48.0)
541 + };
542 +
543 + if ui
544 + .add_sized([80.0, 80.0], egui::Button::new(play_pause_text))
545 + .clicked()
546 + {
547 + self.is_playing = !self.is_playing;
548 + self.last_update = Instant::now();
549 + }
550 +
551 + if ui
552 + .add_sized(
553 + [80.0, 80.0],
554 + egui::Button::new(
555 + egui::RichText::new(ICON_SKIP_PREVIOUS).size(48.0),
556 + ),
557 + )
558 + .clicked()
559 + {
560 + self.scroll_position = 0.0;
561 + self.last_checked_heading_idx = 0;
562 + }
527 563 });
528 − }
529 −
530 − ui.checkbox(&mut self.auto_restart, "Auto Restart");
531 −
532 − ui.add_space(5.0);
533 −
534 − // Font size
535 − ui.horizontal(|ui| {
536 − if ui.add_sized([50.0, 50.0], egui::Button::new(egui::RichText::new(ICON_TEXT_DECREASE).size(32.0))).clicked() {
537 − self.font_size = (self.font_size - 1.0).max(8.0);
538 − }
564 +
539 565 ui.add_space(10.0);
540 − ui.label(egui::RichText::new(format!("{:.0}px", self.font_size)).size(20.0));
566 +
567 + // Speed controls
568 + ui.label("Scroll Speed");
569 + ui.horizontal(|ui| {
570 + if ui
571 + .add_sized(
572 + [60.0, 60.0],
573 + egui::Button::new(egui::RichText::new(ICON_REMOVE).size(36.0)),
574 + )
575 + .clicked()
576 + {
577 + self.scroll_speed = (self.scroll_speed - 10.0).max(10.0);
578 + }
579 + ui.add_space(10.0);
580 + ui.label(
581 + egui::RichText::new(format!("{}px/s", self.scroll_speed as i32))
582 + .size(20.0),
583 + );
584 + ui.add_space(10.0);
585 + if ui
586 + .add_sized(
587 + [60.0, 60.0],
588 + egui::Button::new(egui::RichText::new(ICON_ADD).size(36.0)),
589 + )
590 + .clicked()
591 + {
592 + self.scroll_speed = (self.scroll_speed + 10.0).min(500.0);
593 + }
594 + });
595 +
596 + ui.add_space(20.0);
597 + ui.separator();
541 598 ui.add_space(10.0);
542 − if ui.add_sized([50.0, 50.0], egui::Button::new(egui::RichText::new(ICON_TEXT_INCREASE).size(32.0))).clicked() {
543 − self.font_size = (self.font_size + 1.0).min(72.0);
599 +
600 + // Feature toggles
601 + ui.heading("Settings");
602 + ui.add_space(5.0);
603 +
604 + ui.checkbox(&mut self.pause_at_headings, "Pause at Headings");
605 +
606 + if self.pause_at_headings {
607 + ui.horizontal(|ui| {
608 + ui.label("Duration:");
609 + ui.add(
610 + egui::Slider::new(&mut self.heading_pause_duration, 0.5..=10.0)
611 + .suffix("s")
612 + .text("sec"),
613 + );
614 + });
544 615 }
545 − });
546 −
547 − ui.add_space(20.0);
548 − ui.separator();
549 − ui.add_space(10.0);
550 −
551 − // Theme selection
552 − ui.heading("Theme");
553 − ui.add_space(5.0);
554 −
555 − egui::ComboBox::from_label("Select Theme")
556 − .selected_text(self.current_theme.name.clone())
557 − .show_ui(ui, |ui| {
558 − for theme in &self.available_themes {
559 − if ui.selectable_label(
560 − self.current_theme.name == theme.name,
561 − theme.name.clone()
562 − ).clicked() {
563 − self.current_theme = theme.clone();
564 − // Save theme preference
565 − if let Err(e) = save_theme_preference(&theme.name) {
566 − eprintln!("Failed to save theme preference: {}", e);
567 − }
568 − }
616 +
617 + ui.checkbox(&mut self.auto_restart, "Auto Restart");
618 +
619 + ui.add_space(5.0);
620 +
621 + // Font size
622 + ui.horizontal(|ui| {
623 + if ui
624 + .add_sized(
625 + [50.0, 50.0],
626 + egui::Button::new(
627 + egui::RichText::new(ICON_TEXT_DECREASE).size(32.0),
628 + ),
629 + )
630 + .clicked()
631 + {
632 + self.font_size = (self.font_size - 1.0).max(8.0);
633 + }
634 + ui.add_space(10.0);
635 + ui.label(
636 + egui::RichText::new(format!("{:.0}px", self.font_size)).size(20.0),
637 + );
638 + ui.add_space(10.0);
639 + if ui
640 + .add_sized(
641 + [50.0, 50.0],
642 + egui::Button::new(
643 + egui::RichText::new(ICON_TEXT_INCREASE).size(32.0),
644 + ),
645 + )
646 + .clicked()
647 + {
648 + self.font_size = (self.font_size + 1.0).min(72.0);
569 649 }
570 650 });
571 − });
572 −
651 +
652 + ui.add_space(20.0);
653 + ui.separator();
654 + ui.add_space(10.0);
655 +
656 + // Theme selection
657 + ui.heading("Theme");
658 + ui.add_space(5.0);
659 +
660 + egui::ComboBox::from_label("Select Theme")
661 + .selected_text(self.current_theme.name.clone())
662 + .show_ui(ui, |ui| {
663 + for theme in &self.available_themes {
664 + if ui
665 + .selectable_label(
666 + self.current_theme.name == theme.name,
667 + theme.name.clone(),
668 + )
669 + .clicked()
670 + {
671 + self.current_theme = theme.clone();
672 + // Save theme preference
673 + if let Err(e) = save_theme_preference(&theme.name) {
674 + eprintln!("Failed to save theme preference: {}", e);
675 + }
676 + }
677 + }
678 + });
679 + },
680 + );
681 +
573 682 ui.separator();
574 −
683 +
575 684 // Right column - Content panel
576 685 ui.allocate_ui_with_layout(
577 686 egui::vec2(ui.available_width(), ui.available_height()),
578 687 egui::Layout::top_down(egui::Align::LEFT),
579 688 |ui| {
580 − if let Some(file) = &self.current_file {
581 − ui.heading(file.file_name().unwrap_or_default().to_string_lossy().to_string());
582 − }
583 −
584 − let text_color = Color32::from_rgb(
585 − self.current_theme.text_color[0],
586 − self.current_theme.text_color[1],
587 − self.current_theme.text_color[2]
588 − );
589 −
590 − // Fill remaining height with scroll area
591 − let available_size = ui.available_size();
592 − let scroll_area = ScrollArea::vertical()
593 − .max_height(available_size.y)
594 − .max_width(available_size.x)
595 − .vertical_scroll_offset(self.scroll_position);
596 −
597 − let output = scroll_area.show(ui, |ui| {
598 − ui.set_width(available_size.x - 20.0); // Account for scrollbar
599 −
600 − // Calculate time delta for scrolling
601 − let now = Instant::now();
602 − let dt = now.duration_since(self.last_update).as_secs_f32();
603 − self.last_update = now;
604 − self.update_scroll(dt);
605 −
606 − if !self.parsed_content.is_empty() {
607 − // Custom markdown rendering with colored headings
608 − let lines = self.content.lines().collect::<Vec<&str>>();
609 −
610 − egui::Grid::new("markdown_content")
611 − .num_columns(1)
612 − .spacing([0.0, 5.0])
613 − .striped(false)
614 − .show(ui, |ui| {
615 − for (_i, line) in lines.iter().enumerate() {
616 − let trimmed = line.trim();
617 −
618 − // Detect heading level and extract text without #
619 − let mut heading_level = 0;
620 − let display_text = if trimmed.starts_with("# ") {
621 − heading_level = 1;
622 − trimmed.trim_start_matches("# ")
623 − } else if trimmed.starts_with("## ") {
624 − heading_level = 2;
625 − trimmed.trim_start_matches("## ")
626 − } else if trimmed.starts_with("### ") {
627 − heading_level = 3;
628 − trimmed.trim_start_matches("### ")
629 − } else if trimmed.starts_with("#### ") {
630 − heading_level = 4;
631 − trimmed.trim_start_matches("#### ")
632 − } else if trimmed.starts_with("##### ") {
633 − heading_level = 5;
634 − trimmed.trim_start_matches("##### ")
635 − } else if trimmed.starts_with("###### ") {
636 − heading_level = 6;
637 − trimmed.trim_start_matches("###### ")
638 − } else {
639 − *line
640 − };
641 −
642 − // Apply appropriate color and styling based on whether it's a heading
643 − if heading_level > 0 && heading_level <= self.current_theme.heading_colors.len() {
644 − // It's a heading - use the appropriate heading color
645 − let idx = heading_level - 1;
646 − let heading_color = Color32::from_rgb(
647 − self.current_theme.heading_colors[idx][0],
648 − self.current_theme.heading_colors[idx][1],
649 − self.current_theme.heading_colors[idx][2]
650 − );
651 −
652 − // Adjust font size based on heading level
653 − // H1: 2.0x, H2: 1.8x, H3: 1.6x, H4: 1.4x, H5: 1.2x, H6: 1.1x
654 − // let size_multipliers = [2.0, 1.8, 1.6, 1.4, 1.2, 1.1];
655 − let size_multipliers = [2.0, 1.8, 1.6, 1.4, 1.2, 1.1];
656 − let heading_size = self.font_size * size_multipliers[idx];
657 − ui.style_mut().text_styles.get_mut(&egui::TextStyle::Body).unwrap().size = heading_size;
658 −
659 − ui.colored_label(heading_color, display_text);
660 − ui.end_row();
661 −
662 − // Reset font size to default
663 − ui.style_mut().text_styles.get_mut(&egui::TextStyle::Body).unwrap().size = self.font_size;
664 − } else {
665 − // Regular text - use the formatted text renderer
666 − self.render_formatted_text(ui, display_text, text_color, self.font_size);
667 − ui.end_row();
668 − }
669 − }
670 − });
671 − } else {
672 − ui.colored_label(text_color, "Open a markdown file to begin.");
689 + if let Some(file) = &self.current_file {
690 + ui.heading(
691 + file.file_name()
692 + .unwrap_or_default()
693 + .to_string_lossy()
694 + .to_string(),
695 + );
673 696 }
674 − });
675 −
676 − // Handle end-of-content scrolling behavior
677 − if self.is_playing {
678 − let content_height = output.inner_rect.height();
679 − let available_height = ui.available_height();
680 −
681 − if self.scroll_position >= content_height - available_height {
682 − if self.auto_restart {
683 − self.scroll_position = 0.0;
684 − self.last_checked_heading_idx = 0;
697 +
698 + let text_color = Color32::from_rgb(
699 + self.current_theme.text_color[0],
700 + self.current_theme.text_color[1],
701 + self.current_theme.text_color[2],
702 + );
703 +
704 + // Fill remaining height with scroll area
705 + let available_size = ui.available_size();
706 + let scroll_area = ScrollArea::vertical()
707 + .max_height(available_size.y)
708 + .max_width(available_size.x)
709 + .vertical_scroll_offset(self.scroll_position);
710 +
711 + let output = scroll_area.show(ui, |ui| {
712 + ui.set_width(available_size.x - 20.0); // Account for scrollbar
713 +
714 + // Calculate time delta for scrolling
715 + let now = Instant::now();
716 + let dt = now.duration_since(self.last_update).as_secs_f32();
717 + self.last_update = now;
718 + self.update_scroll(dt);
719 +
720 + if !self.parsed_content.is_empty() {
721 + // Custom markdown rendering with colored headings
722 + let lines = self.content.lines().collect::<Vec<&str>>();
723 +
724 + egui::Grid::new("markdown_content")
725 + .num_columns(1)
726 + .spacing([0.0, 5.0])
727 + .striped(false)
728 + .show(ui, |ui| {
729 + for (_i, line) in lines.iter().enumerate() {
730 + let trimmed = line.trim();
731 +
732 + // Detect heading level and extract text without #
733 + let mut heading_level = 0;
734 + let display_text = if trimmed.starts_with("# ") {
735 + heading_level = 1;
736 + trimmed.trim_start_matches("# ")
737 + } else if trimmed.starts_with("## ") {
738 + heading_level = 2;
739 + trimmed.trim_start_matches("## ")
740 + } else if trimmed.starts_with("### ") {
741 + heading_level = 3;
742 + trimmed.trim_start_matches("### ")
743 + } else if trimmed.starts_with("#### ") {
744 + heading_level = 4;
745 + trimmed.trim_start_matches("#### ")
746 + } else if trimmed.starts_with("##### ") {
747 + heading_level = 5;
748 + trimmed.trim_start_matches("##### ")
749 + } else if trimmed.starts_with("###### ") {
750 + heading_level = 6;
751 + trimmed.trim_start_matches("###### ")
752 + } else {
753 + *line
754 + };
755 +
756 + // Apply appropriate color and styling based on whether it's a heading
757 + if heading_level > 0
758 + && heading_level
759 + <= self.current_theme.heading_colors.len()
760 + {
761 + // It's a heading - use the appropriate heading color
762 + let idx = heading_level - 1;
763 + let heading_color = Color32::from_rgb(
764 + self.current_theme.heading_colors[idx][0],
765 + self.current_theme.heading_colors[idx][1],
766 + self.current_theme.heading_colors[idx][2],
767 + );
768 +
769 + // Adjust font size based on heading level
770 + // H1: 2.0x, H2: 1.8x, H3: 1.6x, H4: 1.4x, H5: 1.2x, H6: 1.1x
771 + // let size_multipliers = [2.0, 1.8, 1.6, 1.4, 1.2, 1.1];
772 + let size_multipliers =
773 + [2.0, 1.8, 1.6, 1.4, 1.2, 1.1];
774 + let heading_size =
775 + self.font_size * size_multipliers[idx];
776 + ui.style_mut()
777 + .text_styles
778 + .get_mut(&egui::TextStyle::Body)
779 + .unwrap()
780 + .size = heading_size;
781 +
782 + ui.colored_label(heading_color, display_text);
783 + ui.end_row();
784 +
785 + // Reset font size to default
786 + ui.style_mut()
787 + .text_styles
788 + .get_mut(&egui::TextStyle::Body)
789 + .unwrap()
790 + .size = self.font_size;
791 + } else {
792 + // Regular text - use the formatted text renderer
793 + self.render_formatted_text(
794 + ui,
795 + display_text,
796 + text_color,
797 + self.font_size,
798 + );
799 + ui.end_row();
800 + }
801 + }
802 + });
685 803 } else {
686 − self.scroll_position = (content_height - available_height).max(0.0);
687 − self.is_playing = false;
804 + ui.colored_label(text_color, "Open a markdown file to begin.");
805 + }
806 + });
807 +
808 + // Handle end-of-content scrolling behavior
809 + if self.is_playing {
810 + let content_height = output.inner_rect.height();
811 + let available_height = ui.available_height();
812 +
813 + if self.scroll_position >= content_height - available_height {
814 + if self.auto_restart {
815 + self.scroll_position = 0.0;
816 + self.last_checked_heading_idx = 0;
817 + } else {
818 + self.scroll_position =
819 + (content_height - available_height).max(0.0);
820 + self.is_playing = false;
821 + }
688 822 }
689 823 }
690 − }
691 − });
824 + },
825 + );
692 826 });
693 827 });
694 −
828 +
695 829 // Request continuous repaint to enable smooth scrolling
696 830 ctx.request_repaint();
697 831 }
@@ -700,64 +834,65 @@ impl App for MarkPrompter {
700 834 // Save theme preference to themes.toml
701 835 fn save_theme_preference(theme_name: &str) -> Result<(), Box<dyn std::error::Error>> {
702 836 let config_path = "themes.toml";
703 −
837 +
704 838 // Read the current themes
705 839 let themes = load_themes_without_preference()?;
706 −
840 +
707 841 // Create the config structure with preference
708 842 #[derive(Serialize)]
709 843 struct ThemesConfigWithPreference {
710 844 selected_theme: String,
711 845 themes: Vec<Theme>,
712 846 }
713 −
847 +
714 848 let config = ThemesConfigWithPreference {
715 849 selected_theme: theme_name.to_string(),
716 850 themes,
717 851 };
718 −
852 +
719 853 let toml_string = toml::to_string(&config)?;
720 854 fs::write(config_path, toml_string)?;
721 855 Ok(())
722 856 }
723 857
724 858 // Load themes and preference from a TOML file
725 −fn load_themes_and_preference() -> Result<(Vec<Theme>, Option<String>), Box<dyn std::error::Error>> {
859 +fn load_themes_and_preference() -> Result<(Vec<Theme>, Option<String>), Box<dyn std::error::Error>>
860 +{
726 861 let config_path = "themes.toml";
727 862 if !std::path::Path::new(config_path).exists() {
728 863 // Create a default theme file if it doesn't exist
729 864 let default_themes = create_default_themes();
730 −
865 +
731 866 println!("Attempting to create themes.toml file...");
732 −
867 +
733 868 // Wrap themes in a structure for TOML serialization
734 869 #[derive(Serialize)]
735 870 struct ThemesConfig {
736 871 selected_theme: Option<String>,
737 872 themes: Vec<Theme>,
738 873 }
739 −
874 +
740 875 let config = ThemesConfig {
741 876 selected_theme: None,
742 877 themes: default_themes.clone(),
743 878 };
744 −
879 +
745 880 let toml_string = toml::to_string(&config)?;
746 881 println!("TOML string generated successfully");
747 882 fs::write(config_path, toml_string)?;
748 883 println!("themes.toml file created successfully");
749 884 return Ok((default_themes, None));
750 885 }
751 −
886 +
752 887 let toml_str = fs::read_to_string(config_path)?;
753 −
888 +
754 889 // Parse TOML with optional selected_theme field
755 890 #[derive(Deserialize)]
756 891 struct ThemesWrapperWithPreference {
757 892 selected_theme: Option<String>,
758 893 themes: Vec<Theme>,
759 894 }
760 −
895 +
761 896 // Try parsing with selected_theme field
762 897 match toml::from_str::<ThemesWrapperWithPreference>(&toml_str) {
763 898 Ok(wrapper) => Ok((wrapper.themes, wrapper.selected_theme)),
@@ -767,7 +902,7 @@ fn load_themes_and_preference() -> Result<(Vec<Theme>, Option<String>), Box<dyn
767 902 struct ThemesWrapper {
768 903 themes: Vec<Theme>,
769 904 }
770 −
905 +
771 906 let wrapper: ThemesWrapper = toml::from_str(&toml_str)?;
772 907 Ok((wrapper.themes, None))
773 908 }
@@ -783,124 +918,124 @@ fn load_themes_without_preference() -> Result<Vec<Theme>, Box<dyn std::error::Er
783 918 // Helper function to create default themes
784 919 fn create_default_themes() -> Vec<Theme> {
785 920 vec![
786 − Theme {
787 − name: "Light".to_string(),
788 − background_color: [240, 240, 245],
789 − text_color: [60, 60, 70],
790 − heading_colors: vec![
791 − [100, 100, 180], // H1
792 − [90, 90, 170], // H2
793 − [80, 80, 160], // H3
794 − [70, 70, 150], // H4
795 − [60, 60, 140], // H5
796 − [50, 50, 130], // H6
797 − ],
798 − },
799 − Theme {
800 − name: "Dark".to_string(),
801 − background_color: [40, 44, 52],
802 − text_color: [220, 223, 228],
803 − heading_colors: vec![
804 − [255, 180, 100], // H1
805 − [230, 160, 90], // H2
806 − [210, 140, 80], // H3
807 − [190, 120, 70], // H4
808 − [170, 100, 60], // H5
809 − [150, 80, 50], // H6
810 − ],
811 − },
812 − Theme {
813 − name: "Solarized".to_string(),
814 − background_color: [0, 43, 54],
815 − text_color: [131, 148, 150],
816 − heading_colors: vec![
817 − [181, 137, 0], // H1
818 − [203, 75, 22], // H2
819 − [220, 50, 47], // H3
820 − [211, 54, 130], // H4
821 − [108, 113, 196], // H5
822 − [38, 139, 210], // H6
823 − ],
824 − },
825 − Theme {
826 − name: "After Dark".to_string(),
827 − background_color: [32, 29, 101], // base-100: #201D65
828 − text_color: [172, 171, 213], // secondary: #ACABD5
829 − heading_colors: vec![
830 − [254, 243, 199], // accent: #fef3c7 - H1
831 − [123, 121, 181], // primary: #7B79B5 - H2
832 − [172, 171, 213], // secondary: #ACABD5 - H3
833 − [125, 211, 252], // info: #7dd3fc - H4
834 − [167, 243, 208], // success: #a7f3d0 - H5
835 − [254, 240, 138], // warning: #fef08a - H6
836 − ],
837 − },
838 − Theme {
839 − name: "Her".to_string(),
840 − background_color: [101, 29, 29], // base-100: #651d1d
841 − text_color: [213, 171, 171], // secondary: #d5abab
842 − heading_colors: vec![
843 − [254, 243, 199], // accent: #fef3c7 - H1
844 − [181, 121, 121], // primary: #b57979 - H2
845 − [213, 171, 171], // secondary: #d5abab - H3
846 − [125, 211, 252], // info: #7dd3fc - H4
847 − [167, 243, 208], // success: #a7f3d0 - H5
848 − [254, 240, 138], // warning: #fef08a - H6
849 − ],
850 − },
851 − Theme {
852 − name: "Forest".to_string(),
853 − background_color: [5, 46, 22], // base-100: #052e16
854 − text_color: [134, 239, 172], // secondary: #86efac
855 − heading_colors: vec![
856 − [254, 243, 199], // accent: #fef3c7 - H1
857 − [74, 222, 128], // primary: #4ade80 - H2
858 − [134, 239, 172], // secondary: #86efac - H3
859 − [125, 211, 252], // info: #7dd3fc - H4
860 − [167, 243, 208], // success: #a7f3d0 - H5
861 − [254, 240, 138], // warning: #fef08a - H6
862 − ],
863 − },
864 − Theme {
865 − name: "Sky".to_string(),
866 − background_color: [8, 47, 73], // base-100: #082f49
867 − text_color: [125, 211, 252], // secondary: #7dd3fc
868 − heading_colors: vec![
869 − [254, 243, 199], // accent: #fef3c7 - H1
870 − [56, 189, 248], // primary: #38bdf8 - H2
871 − [125, 211, 252], // secondary: #7dd3fc - H3
872 − [167, 243, 208], // success: #a7f3d0 - H4
873 − [254, 240, 138], // warning: #fef08a - H5
874 − [252, 165, 165], // error: #fca5a5 - H6
875 − ],
876 − },
877 − Theme {
878 − name: "Clays".to_string(),
879 − background_color: [69, 26, 3], // base-100: #451a03
880 − text_color: [245, 158, 11], // secondary: #f59e0b
881 − heading_colors: vec![
882 − [254, 243, 199], // accent: #fef3c7 - H1
883 − [217, 119, 6], // primary: #d97706 - H2
884 − [245, 158, 11], // secondary: #f59e0b - H3
885 − [125, 211, 252], // info: #7dd3fc - H4
886 − [167, 243, 208], // success: #a7f3d0 - H5
887 − [254, 240, 138], // warning: #fef08a - H6
888 − ],
889 − },
890 − Theme {
891 − name: "Stones".to_string(),
892 − background_color: [41, 37, 36], // base-100: #292524
893 − text_color: [156, 163, 175], // secondary: #9ca3af
894 − heading_colors: vec![
895 − [254, 243, 199], // accent: #fef3c7 - H1
896 − [107, 114, 128], // primary: #6b7280 - H2
897 − [156, 163, 175], // secondary: #9ca3af - H3
898 − [125, 211, 252], // info: #7dd3fc - H4
899 − [167, 243, 208], // success: #a7f3d0 - H5
900 − [254, 240, 138], // warning: #fef08a - H6
901 − ],
902 − },
903 − ]
921 + Theme {
922 + name: "Light".to_string(),
923 + background_color: [240, 240, 245],
924 + text_color: [60, 60, 70],
925 + heading_colors: vec![
926 + [100, 100, 180], // H1
927 + [90, 90, 170], // H2
928 + [80, 80, 160], // H3
929 + [70, 70, 150], // H4
930 + [60, 60, 140], // H5
931 + [50, 50, 130], // H6
932 + ],
933 + },
934 + Theme {
935 + name: "Dark".to_string(),
936 + background_color: [40, 44, 52],
937 + text_color: [220, 223, 228],
938 + heading_colors: vec![
939 + [255, 180, 100], // H1
940 + [230, 160, 90], // H2
941 + [210, 140, 80], // H3
942 + [190, 120, 70], // H4
943 + [170, 100, 60], // H5
944 + [150, 80, 50], // H6
945 + ],
946 + },
947 + Theme {
948 + name: "Solarized".to_string(),
949 + background_color: [0, 43, 54],
950 + text_color: [131, 148, 150],
951 + heading_colors: vec![
952 + [181, 137, 0], // H1
953 + [203, 75, 22], // H2
954 + [220, 50, 47], // H3
955 + [211, 54, 130], // H4
956 + [108, 113, 196], // H5
957 + [38, 139, 210], // H6
958 + ],
959 + },
960 + Theme {
961 + name: "After Dark".to_string(),
962 + background_color: [32, 29, 101], // base-100: #201D65
963 + text_color: [172, 171, 213], // secondary: #ACABD5
964 + heading_colors: vec![
965 + [254, 243, 199], // accent: #fef3c7 - H1
966 + [123, 121, 181], // primary: #7B79B5 - H2
967 + [172, 171, 213], // secondary: #ACABD5 - H3
968 + [125, 211, 252], // info: #7dd3fc - H4
969 + [167, 243, 208], // success: #a7f3d0 - H5
970 + [254, 240, 138], // warning: #fef08a - H6
971 + ],
972 + },
973 + Theme {
974 + name: "Her".to_string(),
975 + background_color: [101, 29, 29], // base-100: #651d1d
976 + text_color: [213, 171, 171], // secondary: #d5abab
977 + heading_colors: vec![
978 + [254, 243, 199], // accent: #fef3c7 - H1
979 + [181, 121, 121], // primary: #b57979 - H2
980 + [213, 171, 171], // secondary: #d5abab - H3
981 + [125, 211, 252], // info: #7dd3fc - H4
982 + [167, 243, 208], // success: #a7f3d0 - H5
983 + [254, 240, 138], // warning: #fef08a - H6
984 + ],
985 + },
986 + Theme {
987 + name: "Forest".to_string(),
988 + background_color: [5, 46, 22], // base-100: #052e16
989 + text_color: [134, 239, 172], // secondary: #86efac
990 + heading_colors: vec![
991 + [254, 243, 199], // accent: #fef3c7 - H1
992 + [74, 222, 128], // primary: #4ade80 - H2
993 + [134, 239, 172], // secondary: #86efac - H3
994 + [125, 211, 252], // info: #7dd3fc - H4
995 + [167, 243, 208], // success: #a7f3d0 - H5
996 + [254, 240, 138], // warning: #fef08a - H6
997 + ],
998 + },
999 + Theme {
1000 + name: "Sky".to_string(),
1001 + background_color: [8, 47, 73], // base-100: #082f49
1002 + text_color: [125, 211, 252], // secondary: #7dd3fc
1003 + heading_colors: vec![
1004 + [254, 243, 199], // accent: #fef3c7 - H1
1005 + [56, 189, 248], // primary: #38bdf8 - H2
1006 + [125, 211, 252], // secondary: #7dd3fc - H3
1007 + [167, 243, 208], // success: #a7f3d0 - H4
1008 + [254, 240, 138], // warning: #fef08a - H5
1009 + [252, 165, 165], // error: #fca5a5 - H6
1010 + ],
1011 + },
1012 + Theme {
1013 + name: "Clays".to_string(),
1014 + background_color: [69, 26, 3], // base-100: #451a03
1015 + text_color: [245, 158, 11], // secondary: #f59e0b
1016 + heading_colors: vec![
1017 + [254, 243, 199], // accent: #fef3c7 - H1
1018 + [217, 119, 6], // primary: #d97706 - H2
1019 + [245, 158, 11], // secondary: #f59e0b - H3
1020 + [125, 211, 252], // info: #7dd3fc - H4
1021 + [167, 243, 208], // success: #a7f3d0 - H5
1022 + [254, 240, 138], // warning: #fef08a - H6
1023 + ],
1024 + },
1025 + Theme {
1026 + name: "Stones".to_string(),
1027 + background_color: [41, 37, 36], // base-100: #292524
1028 + text_color: [156, 163, 175], // secondary: #9ca3af
1029 + heading_colors: vec![
1030 + [254, 243, 199], // accent: #fef3c7 - H1
1031 + [107, 114, 128], // primary: #6b7280 - H2
1032 + [156, 163, 175], // secondary: #9ca3af - H3
1033 + [125, 211, 252], // info: #7dd3fc - H4
1034 + [167, 243, 208], // success: #a7f3d0 - H5
1035 + [254, 240, 138], // warning: #fef08a - H6
1036 + ],
1037 + },
1038 + ]
904 1039 }
905 1040
906 1041 fn main() -> Result<(), eframe::Error> {
@@ -910,10 +1045,10 @@ fn main() -> Result<(), eframe::Error> {
910 1045 .with_min_inner_size([800.0, 600.0]),
911 1046 ..Default::default()
912 1047 };
913 −
1048 +
914 1049 eframe::run_native(
915 1050 "MarkPrompter",
916 1051 options,
917 − Box::new(|cc| Ok(Box::new(MarkPrompter::new(cc))))
1052 + Box::new(|cc| Ok(Box::new(MarkPrompter::new(cc)))),
918 1053 )
919 1054 }

Parents: 7a5272c