Add folder tree
This commit is contained in:
78
src/app.rs
78
src/app.rs
@ -6,7 +6,7 @@ use std::{
|
|||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{file_editor::FileEditor, preferences::Preferences, util::GuiSender};
|
use crate::{file_editor::FileEditor, folder::Folder, preferences::Preferences, util::GuiSender};
|
||||||
use egui::{
|
use egui::{
|
||||||
Align, Button, Color32, Context, FontData, FontDefinitions, Key, Modifiers, PointerButton,
|
Align, Button, Color32, Context, FontData, FontDefinitions, Key, Modifiers, PointerButton,
|
||||||
RichText, ScrollArea, Stroke,
|
RichText, ScrollArea, Stroke,
|
||||||
@ -25,6 +25,9 @@ pub struct App {
|
|||||||
jobs: Jobs,
|
jobs: Jobs,
|
||||||
|
|
||||||
tabs: Vec<(TabId, Tab)>,
|
tabs: Vec<(TabId, Tab)>,
|
||||||
|
|
||||||
|
folders: Vec<Folder>,
|
||||||
|
|
||||||
open_tab_index: Option<usize>,
|
open_tab_index: Option<usize>,
|
||||||
|
|
||||||
next_tab_id: TabId,
|
next_tab_id: TabId,
|
||||||
@ -80,6 +83,7 @@ pub type TabId = usize;
|
|||||||
|
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
OpenFile(FileEditor),
|
OpenFile(FileEditor),
|
||||||
|
OpenFolder(Folder),
|
||||||
MoveFile(TabId, PathBuf),
|
MoveFile(TabId, PathBuf),
|
||||||
CloseTab(TabId),
|
CloseTab(TabId),
|
||||||
// TODO
|
// TODO
|
||||||
@ -102,6 +106,7 @@ impl Default for App {
|
|||||||
tabs: vec![(1, Tab::File(FileEditor::new("note.md")))],
|
tabs: vec![(1, Tab::File(FileEditor::new("note.md")))],
|
||||||
open_tab_index: None,
|
open_tab_index: None,
|
||||||
next_tab_id: 2,
|
next_tab_id: 2,
|
||||||
|
folders: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -194,6 +199,18 @@ impl App {
|
|||||||
|
|
||||||
fn handle_action(&mut self, action: Action) {
|
fn handle_action(&mut self, action: Action) {
|
||||||
match action {
|
match action {
|
||||||
|
Action::OpenFolder(new_folder) => {
|
||||||
|
if let Some(folder) = self
|
||||||
|
.folders
|
||||||
|
.iter_mut()
|
||||||
|
.find(|folder| folder.path() == new_folder.path())
|
||||||
|
{
|
||||||
|
*folder = new_folder;
|
||||||
|
} else {
|
||||||
|
self.folders.push(new_folder);
|
||||||
|
self.folders.sort_by(|a, b| a.name().cmp(b.name()));
|
||||||
|
}
|
||||||
|
}
|
||||||
Action::OpenFile(file_editor) => {
|
Action::OpenFile(file_editor) => {
|
||||||
self.open_tab(Tab::File(file_editor));
|
self.open_tab(Tab::File(file_editor));
|
||||||
}
|
}
|
||||||
@ -262,7 +279,12 @@ impl eframe::App for App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ui.button("Open Folder").clicked() {
|
if ui.button("Open Folder").clicked() {
|
||||||
log::error!("Open Folder not implemented");
|
self.jobs.start(ui.ctx(), move || {
|
||||||
|
let path = rfd::FileDialog::new().pick_folder()?;
|
||||||
|
let name = path.file_name()?.to_string_lossy().to_string();
|
||||||
|
let folder = Folder::NotLoaded { name, path };
|
||||||
|
Some(Action::OpenFolder(folder))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
@ -277,8 +299,8 @@ impl eframe::App for App {
|
|||||||
let can_save_file = self
|
let can_save_file = self
|
||||||
.open_tab_index
|
.open_tab_index
|
||||||
.and_then(|i| self.tabs.get(i))
|
.and_then(|i| self.tabs.get(i))
|
||||||
.and_then(|(id, tab)| match tab {
|
.map(|(id, tab)| match tab {
|
||||||
Tab::File(file_editor) => Some((*id, file_editor)),
|
Tab::File(file_editor) => (*id, file_editor),
|
||||||
})
|
})
|
||||||
.and_then(|(_, file_editor)| file_editor.path().zip(Some(file_editor)))
|
.and_then(|(_, file_editor)| file_editor.path().zip(Some(file_editor)))
|
||||||
.is_some();
|
.is_some();
|
||||||
@ -287,12 +309,11 @@ impl eframe::App for App {
|
|||||||
self.save_active_tab(ui.ctx());
|
self.save_active_tab(ui.ctx());
|
||||||
}
|
}
|
||||||
|
|
||||||
let open_file =
|
let open_file = self.open_tab_index.and_then(|i| self.tabs.get(i)).map(
|
||||||
self.open_tab_index
|
|(id, tab)| match tab {
|
||||||
.and_then(|i| self.tabs.get(i))
|
Tab::File(file_editor) => (*id, file_editor),
|
||||||
.and_then(|(id, tab)| match tab {
|
},
|
||||||
Tab::File(file_editor) => Some((*id, file_editor)),
|
);
|
||||||
});
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if ui
|
if ui
|
||||||
@ -351,6 +372,39 @@ impl eframe::App for App {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
egui::SidePanel::left("file browser")
|
||||||
|
.resizable(true)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
if ui.button("refresh").clicked() {
|
||||||
|
for folder in &mut self.folders {
|
||||||
|
folder.unload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollArea::both().auto_shrink(false).show(ui, |ui| {
|
||||||
|
self.folders.retain_mut(|folder| {
|
||||||
|
let response = folder.show(ui);
|
||||||
|
|
||||||
|
if let Some(file_path) = response.open_file {
|
||||||
|
let file_path = file_path.to_owned();
|
||||||
|
self.jobs.start(ui.ctx(), move || {
|
||||||
|
let text = fs::read_to_string(&file_path)
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!("Failed to read {file_path:?}: {e}")
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let editor = FileEditor::from_file(file_path, &text);
|
||||||
|
Some(Action::OpenFile(editor))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete on right-click
|
||||||
|
!response.clicked_by(PointerButton::Secondary)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
if let Some(Tab::File(file_editor)) = self
|
if let Some(Tab::File(file_editor)) = self
|
||||||
.open_tab_index
|
.open_tab_index
|
||||||
@ -389,8 +443,8 @@ impl App {
|
|||||||
let open_file = self
|
let open_file = self
|
||||||
.open_tab_index
|
.open_tab_index
|
||||||
.and_then(|i| self.tabs.get_mut(i))
|
.and_then(|i| self.tabs.get_mut(i))
|
||||||
.and_then(|(id, tab)| match tab {
|
.map(|(id, tab)| match tab {
|
||||||
Tab::File(file_editor) => Some((*id, file_editor)),
|
Tab::File(file_editor) => (*id, file_editor),
|
||||||
})
|
})
|
||||||
.and_then(|(_, file_editor)| {
|
.and_then(|(_, file_editor)| {
|
||||||
file_editor
|
file_editor
|
||||||
|
|||||||
218
src/folder.rs
Normal file
218
src/folder.rs
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
use std::{
|
||||||
|
fs::read_dir,
|
||||||
|
mem,
|
||||||
|
ops::Deref,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::mpsc,
|
||||||
|
thread,
|
||||||
|
};
|
||||||
|
|
||||||
|
use egui::{Response, Ui};
|
||||||
|
use eyre::{Context, OptionExt, eyre};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub enum Folder {
|
||||||
|
NotLoaded {
|
||||||
|
name: String,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
Loading {
|
||||||
|
name: String,
|
||||||
|
path: PathBuf,
|
||||||
|
recv: mpsc::Receiver<LoadedFolder>,
|
||||||
|
},
|
||||||
|
Loaded(LoadedFolder),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LoadedFolder {
|
||||||
|
pub name: String,
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub child_folders: Vec<Folder>,
|
||||||
|
pub child_files: Vec<File>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct File {
|
||||||
|
pub name: String,
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FolderResponse<'a> {
|
||||||
|
inner: Response,
|
||||||
|
pub open_file: Option<&'a Path>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for FolderResponse<'_> {
|
||||||
|
type Target = Response;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoadedFolder {
|
||||||
|
pub fn show<'a>(&'a mut self, ui: &mut Ui) -> FolderResponse<'a> {
|
||||||
|
let mut open_file = None;
|
||||||
|
let inner = ui
|
||||||
|
.collapsing(&self.name, |ui| {
|
||||||
|
for folder in &mut self.child_folders {
|
||||||
|
open_file = open_file.or(folder.show(ui).open_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
for file in &mut self.child_files {
|
||||||
|
if ui.button(&file.name).clicked() {
|
||||||
|
open_file = Some(file.path.as_path())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.header_response;
|
||||||
|
|
||||||
|
FolderResponse { inner, open_file }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(path: PathBuf) -> eyre::Result<Self> {
|
||||||
|
let name = path
|
||||||
|
.file_name()
|
||||||
|
.ok_or_eyre("Path is missing a file-name")?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let mut child_folders = vec![];
|
||||||
|
let mut child_files = vec![];
|
||||||
|
|
||||||
|
for entry in read_dir(&path).with_context(|| eyre!("Couldn't read dir {path:?}"))? {
|
||||||
|
let entry = entry.with_context(|| eyre!("Couldn't read dir {path:?}"))?;
|
||||||
|
let path = entry.path();
|
||||||
|
let name = path
|
||||||
|
.file_name()
|
||||||
|
.ok_or_eyre("Path is missing a file-name")?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let file_type = entry.file_type()?;
|
||||||
|
|
||||||
|
if file_type.is_symlink() {
|
||||||
|
log::error!("Symlinks not yet supported, skipping {path:?}");
|
||||||
|
continue;
|
||||||
|
} else if file_type.is_file() {
|
||||||
|
child_files.push(File { name, path });
|
||||||
|
} else if file_type.is_dir() {
|
||||||
|
child_folders.push(Folder::NotLoaded { name, path });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let folder = LoadedFolder {
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
child_folders,
|
||||||
|
child_files,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(folder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Folder {
|
||||||
|
fn load(&mut self, ui: &mut Ui) -> Option<&mut LoadedFolder> {
|
||||||
|
if let Folder::NotLoaded { name, path } = self {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
{
|
||||||
|
let path = path.clone();
|
||||||
|
let ctx = ui.ctx().clone();
|
||||||
|
thread::spawn(move || match LoadedFolder::load(path) {
|
||||||
|
Err(e) => log::error!("Failed to load folder: {e}"),
|
||||||
|
Ok(folder) => {
|
||||||
|
let _ = tx.send(folder);
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
*self = Folder::Loading {
|
||||||
|
name: mem::take(name),
|
||||||
|
path: mem::take(path),
|
||||||
|
recv: rx,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Folder::Loading { recv, .. } = self {
|
||||||
|
match recv.try_recv() {
|
||||||
|
Ok(folder) => *self = Folder::Loaded(folder),
|
||||||
|
Err(_) => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Folder::Loaded(folder) = self else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show<'a>(&'a mut self, ui: &mut Ui) -> FolderResponse<'a> {
|
||||||
|
self.load(ui);
|
||||||
|
|
||||||
|
if let Folder::Loaded(folder) = self {
|
||||||
|
return folder.show(ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
FolderResponse {
|
||||||
|
inner: ui.label(self.name()),
|
||||||
|
open_file: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &Path {
|
||||||
|
match self {
|
||||||
|
Folder::NotLoaded { path, .. } => path,
|
||||||
|
Folder::Loading { path, .. } => path,
|
||||||
|
Folder::Loaded(folder) => &folder.path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Folder::NotLoaded { name, .. } => name,
|
||||||
|
Folder::Loading { name, .. } => name,
|
||||||
|
Folder::Loaded(folder) => &folder.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unload(&mut self) {
|
||||||
|
let (name, path) = match self {
|
||||||
|
Folder::NotLoaded { .. } => return,
|
||||||
|
Folder::Loading { name, path, .. } => (name, path),
|
||||||
|
Folder::Loaded(folder) => (&mut folder.name, &mut folder.path),
|
||||||
|
};
|
||||||
|
|
||||||
|
*self = Folder::NotLoaded {
|
||||||
|
name: mem::take(name),
|
||||||
|
path: mem::take(path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Folder {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
self.path().serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Folder {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use serde::de::Error;
|
||||||
|
let path = PathBuf::deserialize(deserializer)?;
|
||||||
|
let name = path
|
||||||
|
.file_name()
|
||||||
|
.ok_or(D::Error::custom("Path is missing a file-name"))?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
Ok(Folder::NotLoaded { name, path })
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -289,11 +289,11 @@ impl Handwriting {
|
|||||||
_ => Some(next),
|
_ => Some(next),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.cloned()
|
|
||||||
.filter(|event| {
|
.filter(|event| {
|
||||||
// FIXME: pinenote: PointerMoved are duplicated after the MouseMoved events
|
// FIXME: pinenote: PointerMoved are duplicated after the MouseMoved events
|
||||||
cfg!(not(feature = "pinenote")) || !matches!(event, Event::PointerMoved(..))
|
cfg!(not(feature = "pinenote")) || !matches!(event, Event::PointerMoved(..))
|
||||||
})
|
})
|
||||||
|
.cloned()
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -459,6 +459,8 @@ impl Handwriting {
|
|||||||
mesh_context: MeshContext,
|
mesh_context: MeshContext,
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
) {
|
) {
|
||||||
|
// TODO: don't tesselate and rasterize on the GUI thread
|
||||||
|
|
||||||
self.last_mesh_ctx = Some(mesh_context);
|
self.last_mesh_ctx = Some(mesh_context);
|
||||||
|
|
||||||
self.refresh_texture = false;
|
self.refresh_texture = false;
|
||||||
@ -646,6 +648,7 @@ impl FromStr for Handwriting {
|
|||||||
.wrap_err("Failed to decode painting data from base64")?;
|
.wrap_err("Failed to decode painting data from base64")?;
|
||||||
|
|
||||||
// HACK: first iteration of disk format did not have version header
|
// HACK: first iteration of disk format did not have version header
|
||||||
|
//let mut bytes = bytes;
|
||||||
//bytes.insert(0, 0);
|
//bytes.insert(0, 0);
|
||||||
//bytes.insert(0, 1);
|
//bytes.insert(0, 1);
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ pub mod constants;
|
|||||||
pub mod custom_code_block;
|
pub mod custom_code_block;
|
||||||
pub mod easy_mark;
|
pub mod easy_mark;
|
||||||
pub mod file_editor;
|
pub mod file_editor;
|
||||||
|
pub mod folder;
|
||||||
pub mod handwriting;
|
pub mod handwriting;
|
||||||
pub mod preferences;
|
pub mod preferences;
|
||||||
pub mod rasterizer;
|
pub mod rasterizer;
|
||||||
|
|||||||
@ -165,10 +165,8 @@ fn point_in_triangle(point: Pos2, triangle: [&Vertex; 3]) -> PointInTriangle {
|
|||||||
// Normalize the weights.
|
// Normalize the weights.
|
||||||
let weights = areas.map(|area| area / triangle_area);
|
let weights = areas.map(|area| area / triangle_area);
|
||||||
|
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) && weights.into_iter().any(f32::is_nan) {
|
||||||
if weights.into_iter().any(f32::is_nan) {
|
panic!("weights must not be NaN! {weights:?} {triangle_area:?} {areas:?} {sides:?}");
|
||||||
panic!("weights must not be NaN! {weights:?} {triangle_area:?} {areas:?} {sides:?}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PointInTriangle { inside, weights }
|
PointInTriangle { inside, weights }
|
||||||
|
|||||||
Reference in New Issue
Block a user