Add folder tree

This commit is contained in:
2025-06-19 23:09:41 +02:00
parent 4e9eacc7b0
commit b39419888b
5 changed files with 291 additions and 17 deletions

View File

@ -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
View 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 })
}
}

View File

@ -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);

View File

@ -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;

View File

@ -165,11 +165,9 @@ 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 }
} }