Add App
This commit is contained in:
356
src/file_editor.rs
Normal file
356
src/file_editor.rs
Normal file
@ -0,0 +1,356 @@
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fmt::{self, Display},
|
||||
ops::{Div as _, Sub as _},
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use egui::{
|
||||
Align, Button, DragAndDrop, Frame, Layout, ScrollArea, Ui, UiBuilder, Vec2, Widget as _, vec2,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
custom_code_block::{MdItem, iter_lines_and_code_blocks},
|
||||
painting::{self, Handwriting, HandwritingStyle},
|
||||
preferences::Preferences,
|
||||
text_editor::MdTextEdit,
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct FileEditor {
|
||||
title: String,
|
||||
pub path: Option<PathBuf>,
|
||||
pub buffer: Vec<BufferItem>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub enum BufferItem {
|
||||
Text(MdTextEdit),
|
||||
Handwriting(Handwriting),
|
||||
}
|
||||
|
||||
impl FileEditor {
|
||||
pub fn new(title: impl Into<String>) -> Self {
|
||||
let buffer = vec![BufferItem::Text(MdTextEdit::new())];
|
||||
Self {
|
||||
title: title.into(),
|
||||
path: None,
|
||||
buffer,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_file(file_path: PathBuf, contents: &str) -> Self {
|
||||
let file_title = file_path
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| String::from("untitled.md"));
|
||||
Self {
|
||||
title: file_title,
|
||||
path: Some(file_path),
|
||||
..FileEditor::from(contents)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn title(&self) -> &str {
|
||||
&self.title
|
||||
}
|
||||
|
||||
pub fn path(&self) -> Option<&Path> {
|
||||
self.path.as_deref()
|
||||
}
|
||||
|
||||
pub fn show(&mut self, ui: &mut Ui, preferences: &Preferences) {
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
ui.heading(&self.title);
|
||||
|
||||
const MAX_NOTE_WIDTH: f32 = 600.0;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("new");
|
||||
if ui.button("text").clicked() {
|
||||
self.buffer.push(BufferItem::Text(Default::default()));
|
||||
}
|
||||
if ui.button("writing").clicked() {
|
||||
self.buffer
|
||||
.push(BufferItem::Handwriting(Default::default()));
|
||||
}
|
||||
});
|
||||
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let side_padding = ui.available_width().sub(MAX_NOTE_WIDTH).max(0.0).div(2.0);
|
||||
ui.add_space(side_padding);
|
||||
ui.vertical(|ui| {
|
||||
ui.set_max_width(MAX_NOTE_WIDTH);
|
||||
self.show_contents(ui, preferences);
|
||||
});
|
||||
ui.add_space(side_padding);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn show_contents(&mut self, ui: &mut Ui, preferences: &Preferences) {
|
||||
if self.buffer.is_empty() {
|
||||
self.buffer.push(BufferItem::Text(Default::default()));
|
||||
}
|
||||
|
||||
struct DraggingItem {
|
||||
index: usize,
|
||||
}
|
||||
|
||||
let mut drop_from_to: Option<(usize, usize)> = None;
|
||||
let is_dragging = DragAndDrop::has_payload_of_type::<DraggingItem>(ui.ctx());
|
||||
let drag_zone_height = 10.0;
|
||||
|
||||
// Iterate over buffer items using `retain` so that we can handle deletions
|
||||
let mut i = 0usize..;
|
||||
let len = self.buffer.len();
|
||||
self.buffer.retain_mut(|item| {
|
||||
let i = i.next().unwrap();
|
||||
let is_first = i == 0;
|
||||
let is_last = i == len - 1;
|
||||
|
||||
let mut retain = true;
|
||||
|
||||
if is_dragging {
|
||||
let (_, drop) = ui.dnd_drop_zone::<DraggingItem, _>(Frame::NONE, |ui| {
|
||||
ui.set_min_size(vec2(ui.available_width(), drag_zone_height));
|
||||
});
|
||||
if let Some(drop) = drop {
|
||||
drop_from_to = Some((drop.index, i));
|
||||
}
|
||||
} else {
|
||||
// the dnd_drop_zone adds 3pts work of extra space
|
||||
ui.add_space(drag_zone_height + 3.0);
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
// We don't know how tall the buffer item will be, so we'll reserve
|
||||
// some horizontal space here and come back to drawing the dragger
|
||||
// later.
|
||||
let (dragger_id, mut dragger_rect) = ui.allocate_space(Vec2::new(20.0, 1.0));
|
||||
|
||||
// Leave some space at the end for the delete button..
|
||||
let w = ui.available_width();
|
||||
let item_size = Vec2::new(w - 20.0, 0.0);
|
||||
|
||||
let item_response = ui.allocate_ui(item_size, |ui| match item {
|
||||
BufferItem::Text(text_edit) => {
|
||||
text_edit.ui(ui);
|
||||
}
|
||||
BufferItem::Handwriting(painting) => {
|
||||
let style = HandwritingStyle {
|
||||
animate: preferences.animations,
|
||||
..HandwritingStyle::from_theme(ui.ctx().theme())
|
||||
};
|
||||
painting.ui(&style, ui);
|
||||
}
|
||||
});
|
||||
|
||||
// Delete-button
|
||||
if ui.button("x").clicked() {
|
||||
retain = false;
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
// Draw the dragger using the height from the buffer item
|
||||
dragger_rect.set_height(item_response.response.rect.height());
|
||||
|
||||
// Controls for moving the buffer item
|
||||
ui.allocate_new_ui(
|
||||
UiBuilder::new()
|
||||
.max_rect(dragger_rect)
|
||||
.layout(Layout::top_down(Align::Center)),
|
||||
|ui| {
|
||||
let up_button_response = ui.add_enabled(!is_first, Button::new("⇡"));
|
||||
if up_button_response.clicked() {
|
||||
drop_from_to = Some((i, i - 1));
|
||||
}
|
||||
|
||||
ui.dnd_drag_source(dragger_id, DraggingItem { index: i }, |ui| {
|
||||
Button::new("≡")
|
||||
.min_size(
|
||||
// Use all available height, save for the height taken up by
|
||||
// the up/down buttons + padding. Assume down-button is the
|
||||
// equally tall as the up-button.
|
||||
dragger_rect.size()
|
||||
- Vec2::Y * (up_button_response.rect.height() * 2.0 + 4.0),
|
||||
)
|
||||
.ui(ui);
|
||||
});
|
||||
|
||||
if ui.add_enabled(!is_last, Button::new("⇣")).clicked() {
|
||||
drop_from_to = Some((i, i + 2));
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
retain
|
||||
});
|
||||
|
||||
if is_dragging {
|
||||
let (_, drop) = ui.dnd_drop_zone::<DraggingItem, _>(Frame::NONE, |ui| {
|
||||
ui.set_min_size(vec2(ui.available_width(), drag_zone_height));
|
||||
});
|
||||
if let Some(drop) = drop {
|
||||
drop_from_to = Some((drop.index, self.buffer.len()));
|
||||
}
|
||||
} else {
|
||||
// the dnd_drop_zone adds 3.0pts work of extra space
|
||||
ui.add_space(drag_zone_height + 3.0);
|
||||
}
|
||||
|
||||
// Handle drag-and-dropping buffer items
|
||||
// TODO: make sure nothing was removed from self.buffer this frame
|
||||
if let Some((from, to)) = drop_from_to {
|
||||
if from < self.buffer.len() {
|
||||
match from.cmp(&to) {
|
||||
Ordering::Greater => {
|
||||
let item = self.buffer.remove(from);
|
||||
self.buffer.insert(to, item);
|
||||
}
|
||||
Ordering::Less => {
|
||||
let item = self.buffer.remove(from);
|
||||
self.buffer.insert(to - 1, item);
|
||||
}
|
||||
Ordering::Equal => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_path(&mut self, new_path: PathBuf) {
|
||||
let Some(title) = new_path.file_name() else {
|
||||
log::error!("No filename in path {new_path:?}");
|
||||
return;
|
||||
};
|
||||
self.title = title.to_string_lossy().to_string();
|
||||
self.path = Some(new_path);
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for BufferItem {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
BufferItem::Text(md_text_edit) => Display::fmt(md_text_edit, f),
|
||||
BufferItem::Handwriting(handwriting) => Display::fmt(handwriting, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FileEditor {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut first = true;
|
||||
for item in &self.buffer {
|
||||
if !first {
|
||||
writeln!(f)?;
|
||||
}
|
||||
|
||||
first = false;
|
||||
|
||||
write!(f, "{item}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for FileEditor {
|
||||
fn from(s: &str) -> Self {
|
||||
let mut editor = FileEditor::new("note.md");
|
||||
let buffer = &mut editor.buffer;
|
||||
|
||||
let push_text = |buffer: &mut Vec<BufferItem>, text| match buffer.last_mut() {
|
||||
Some(BufferItem::Text(text_edit)) => text_edit.text.push_str(text),
|
||||
_ => {
|
||||
let mut text_edit = MdTextEdit::new();
|
||||
text_edit.text.push_str(text);
|
||||
buffer.push(BufferItem::Text(text_edit));
|
||||
}
|
||||
};
|
||||
|
||||
for item in iter_lines_and_code_blocks(s) {
|
||||
match item {
|
||||
MdItem::Line(line) => push_text(buffer, line),
|
||||
MdItem::CodeBlock { key, content, span } => match key {
|
||||
painting::CODE_BLOCK_KEY => match Handwriting::from_str(span) {
|
||||
Ok(handwriting) => {
|
||||
if let Some(BufferItem::Text(text_edit)) = buffer.last_mut() {
|
||||
if text_edit.text.ends_with('\n') {
|
||||
text_edit.text.pop();
|
||||
if text_edit.text.is_empty() {
|
||||
buffer.pop();
|
||||
}
|
||||
}
|
||||
};
|
||||
buffer.push(BufferItem::Handwriting(handwriting))
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to decode handwriting {content:?}: {e}");
|
||||
push_text(buffer, span);
|
||||
}
|
||||
},
|
||||
_ => push_text(buffer, span),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
editor
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use crate::file_editor::BufferItem;
|
||||
|
||||
use super::FileEditor;
|
||||
|
||||
#[test]
|
||||
fn from_str_and_back_1() {
|
||||
let markdown = r#"
|
||||
# Hello world!
|
||||
|
||||
This is some text.
|
||||
Here's some handwriting:
|
||||
```handwriting
|
||||
DgB0UUlNeFFJTX9RUE2pUYZNDlIATotSjk4AUwxPaFODT89T608UVBtQL1QqUDtULlBDVDFQSVQuUA==
|
||||
```
|
||||
|
||||
And here's some more text :D
|
||||
```
|
||||
with a regular code-block!
|
||||
```"#;
|
||||
|
||||
println!("{markdown}");
|
||||
println!();
|
||||
println!();
|
||||
println!("{markdown:?}");
|
||||
println!();
|
||||
println!();
|
||||
|
||||
let file_editor = FileEditor::from(markdown);
|
||||
|
||||
for item in &file_editor.buffer {
|
||||
match item {
|
||||
BufferItem::Text(md_text_edit) => {
|
||||
println!("{:?}", md_text_edit.text);
|
||||
}
|
||||
BufferItem::Handwriting(_) => {
|
||||
println!("<handwriting>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!();
|
||||
|
||||
let serialized = file_editor.to_string();
|
||||
assert_eq!(
|
||||
markdown, serialized,
|
||||
"FileEditor should preserve formatting"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user