From 05368dea2c996dd880c69edda940ac6ca226a5b1 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 27 Oct 2023 11:43:30 -0600 Subject: [PATCH] Implement project tree --- Cargo.lock | 84 +++++++++++----------- src/main.rs | 184 ++++++++++++++++++++++++++++++++++++++++++------- src/project.rs | 66 ++++++++++++++++-- 3 files changed, 259 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6bab1c6..716a443 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,7 +303,7 @@ dependencies = [ "log", "parking", "polling 2.8.0", - "rustix 0.37.26", + "rustix 0.37.27", "slab", "socket2", "waker-fn", @@ -331,7 +331,7 @@ dependencies = [ "cfg-if", "event-listener 3.0.0", "futures-lite", - "rustix 0.38.20", + "rustix 0.38.21", "windows-sys 0.48.0", ] @@ -358,7 +358,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 0.38.20", + "rustix 0.38.21", "signal-hook-registry", "slab", "windows-sys 0.48.0", @@ -405,7 +405,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4d45f362125ed144544e57b0ec6de8fd6a296d41a6252fc4a20c0cf12e9ed3a" dependencies = [ - "rustix 0.38.20", + "rustix 0.38.21", "tempfile", "windows-sys 0.48.0", ] @@ -624,7 +624,7 @@ dependencies = [ "bitflags 2.4.1", "log", "polling 3.2.0", - "rustix 0.38.20", + "rustix 0.38.21", "slab", "thiserror", ] @@ -636,7 +636,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" dependencies = [ "calloop 0.12.3", - "rustix 0.38.20", + "rustix 0.38.21", "wayland-backend 0.3.2", "wayland-client 0.31.1", ] @@ -839,7 +839,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -853,7 +853,7 @@ dependencies = [ [[package]] name = "cosmic-config-derive" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "quote", "syn 1.0.109", @@ -916,7 +916,7 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "almost", "cosmic-config", @@ -2047,7 +2047,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "iced" version = "0.10.0" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "iced_accessibility", "iced_core", @@ -2063,7 +2063,7 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "accesskit", "accesskit_unix", @@ -2073,7 +2073,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.10.0" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "bitflags 1.3.2", "iced_accessibility", @@ -2088,7 +2088,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.7.0" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "futures", "iced_core", @@ -2100,7 +2100,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.9.0" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "bitflags 1.3.2", "bytemuck", @@ -2118,7 +2118,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -2131,7 +2131,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.1.1" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "iced_accessibility", "iced_core", @@ -2143,7 +2143,7 @@ dependencies = [ [[package]] name = "iced_sctk" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "enum-repr", "float-cmp", @@ -2167,7 +2167,7 @@ dependencies = [ [[package]] name = "iced_style" version = "0.9.0" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "iced_core", "once_cell", @@ -2177,7 +2177,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "bytemuck", "cosmic-text 0.9.0", @@ -2195,7 +2195,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.11.1" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "bitflags 1.3.2", "bytemuck", @@ -2217,7 +2217,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.1.3" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "iced_renderer", "iced_runtime", @@ -2232,7 +2232,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.10.0" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "iced_graphics", "iced_runtime", @@ -2357,7 +2357,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", - "rustix 0.38.20", + "rustix 0.38.21", "windows-sys 0.48.0", ] @@ -2479,7 +2479,7 @@ checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "libcosmic" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic?branch=menu#d8172e3a778a7a4b8212e590a798ee0f8ad1b49e" +source = "git+https://github.com/pop-os/libcosmic?branch=menu#4e5e1a7f54ed117c066289758c7b3dfe0487be2c" dependencies = [ "apply", "ashpd", @@ -3483,7 +3483,7 @@ dependencies = [ "cfg-if", "concurrent-queue", "pin-project-lite", - "rustix 0.38.20", + "rustix 0.38.21", "tracing", "windows-sys 0.48.0", ] @@ -3841,9 +3841,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.26" +version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84f3f8f960ed3b5a59055428714943298bf3fa2d4a1d53135084e0544829d995" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags 1.3.2", "errno", @@ -3855,9 +3855,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.20" +version = "0.38.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" dependencies = [ "bitflags 2.4.1", "errno", @@ -4375,14 +4375,14 @@ checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" [[package]] name = "tempfile" -version = "3.8.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand 2.0.1", - "redox_syscall 0.3.5", - "rustix 0.38.20", + "redox_syscall 0.4.1", + "rustix 0.38.21", "windows-sys 0.48.0", ] @@ -4548,14 +4548,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3efaf127c78d5339cc547cce4e4d973bd5e4f56e949a06d091c082ebeef2f800" +checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.20.5", + "toml_edit 0.20.7", ] [[package]] @@ -4580,9 +4580,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.20.5" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "782bf6c2ddf761c1e7855405e8975472acf76f7f36d0d4328bd3b7a2fae12a85" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ "indexmap 2.0.2", "serde", @@ -5854,18 +5854,18 @@ checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" [[package]] name = "zerocopy" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ba595b9f2772fbee2312de30eeb80ec773b4cb2f1e8098db024afadda6c06f" +checksum = "c552e97c5a9b90bc8ddc545b5106e798807376356688ebaa3aee36f44f8c4b9e" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772666c41fb6dceaf520b564b962d738a8e1a83b41bd48945f50837aed78bb1d" +checksum = "964bc0588d7ac1c0243d0427ef08482618313702bbb014806cb7ab3da34d3d99" dependencies = [ "proc-macro2", "quote", diff --git a/src/main.rs b/src/main.rs index b675ed0..ef219b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,12 +8,12 @@ use cosmic::{ Alignment, Length, Limits, }, style, - widget::{self, button, icon, segmented_button, view_switcher}, + widget::{self, button, icon, nav_bar, segmented_button, view_switcher}, ApplicationExt, Element, }; use cosmic_text::{FontSystem, SyntaxSystem, ViMode}; use std::{ - env, + env, fs, path::{Path, PathBuf}, sync::Mutex, }; @@ -21,7 +21,7 @@ use std::{ use self::menu::menu_bar; mod menu; -use self::project::Project; +use self::project::ProjectNode; mod project; use self::tab::Tab; @@ -60,7 +60,7 @@ impl Config { pub struct App { core: Core, - projects: Vec, + nav_model: segmented_button::SingleSelectModel, tab_model: segmented_button::SingleSelectModel, config: Config, } @@ -87,13 +87,95 @@ impl App { self.tab_model.active_data_mut() } - pub fn open_project>(&mut self, path: P) { - match Project::new(&path) { - Ok(project) => self.projects.push(project), + fn open_folder>(&mut self, path: P, mut position: u16, indent: u16) { + let read_dir = match fs::read_dir(&path) { + Ok(ok) => ok, Err(err) => { - log::error!("failed to open '{}': {}", path.as_ref().display(), err); + log::error!("failed to read directory {:?}: {}", path.as_ref(), err); + return; } + }; + + let mut nodes = Vec::new(); + for entry_res in read_dir { + let entry = match entry_res { + Ok(ok) => ok, + Err(err) => { + log::error!( + "failed to read entry in directory {:?}: {}", + path.as_ref(), + err + ); + continue; + } + }; + + let entry_path = entry.path(); + let node = match ProjectNode::new(&entry_path) { + Ok(ok) => ok, + Err(err) => { + log::error!( + "failed to open directory {:?} entry {:?}: {}", + path.as_ref(), + entry_path, + err + ); + continue; + } + }; + nodes.push(node); } + + nodes.sort_by(|a, b| a.name().cmp(b.name())); + + for node in nodes { + self.nav_model + .insert() + .position(position) + .indent(indent) + .icon(icon::from_name(node.icon_name()).size(16).icon()) + .text(node.name().to_string()) + .data(node); + + position += 1; + } + } + + pub fn open_project>(&mut self, path: P) { + let node = match ProjectNode::new(&path) { + Ok(ok) => ok, + Err(err) => { + log::error!("failed to open project {:?}: {}", path.as_ref(), err); + return; + } + }; + + let project = match node { + ProjectNode::Folder { name, path, .. } => ProjectNode::Root { + name, + path, + open: true, + }, + _ => { + log::error!( + "failed to open project {:?}: not a directory", + path.as_ref() + ); + return; + } + }; + + let id = self + .nav_model + .insert() + .icon(icon::from_name(project.icon_name()).size(16).icon()) + .text(project.name().to_string()) + .data(project) + .id(); + + let position = self.nav_model.position(id).unwrap_or(0); + + self.open_folder(&path, position + 1, 1); } pub fn open_tab(&mut self, path_opt: Option) { @@ -105,8 +187,8 @@ impl App { self.tab_model .insert() .text(tab.title()) - .icon(icon::from_name("text-x-generic").icon()) - .data(tab) + .icon(icon::from_name("text-x-generic").size(16).icon()) + .data::(tab) .closable() .activate(); } @@ -148,7 +230,7 @@ impl cosmic::Application for App { fn init(core: Core, _flags: Self::Flags) -> (Self, Command) { let mut app = App { core, - projects: Vec::new(), + nav_model: nav_bar::Model::builder().build(), tab_model: segmented_button::Model::builder().build(), config: Config::new(), }; @@ -162,6 +244,11 @@ impl cosmic::Application for App { } } + // Show nav bar only if project is provided + if app.core.nav_bar_active() != app.nav_model.iter().next().is_some() { + app.core.nav_bar_toggle(); + } + // Open an empty file if no arguments provided if app.tab_model.iter().next().is_none() { app.open_tab(None); @@ -171,7 +258,65 @@ impl cosmic::Application for App { (app, command) } - fn update(&mut self, message: Message) -> Command { + fn nav_model(&self) -> Option<&nav_bar::Model> { + Some(&self.nav_model) + } + + fn on_nav_select(&mut self, id: nav_bar::Id) -> Command { + let node = match self.nav_model.data_mut::(id) { + Some(node) => { + match node { + ProjectNode::Folder { open, .. } => { + *open = !*open; + } + _ => {} + } + node.clone() + } + None => { + log::warn!("no path found for id {:?}", id); + return Command::none(); + } + }; + + self.nav_model + .icon_set(id, icon::from_name(node.icon_name()).size(16).icon()); + + match node { + ProjectNode::Root { .. } => { + log::warn!("TODO: root node click"); + Command::none() + } + ProjectNode::Folder { path, open, .. } => { + let position = self.nav_model.position(id).unwrap_or(0); + let indent = self.nav_model.indent(id).unwrap_or(0); + if open { + self.open_folder(path, position + 1, indent + 1); + } else { + loop { + let child_id = match self.nav_model.entity_at(position + 1) { + Some(some) => some, + None => break, + }; + + if self.nav_model.indent(child_id).unwrap_or(0) > indent { + self.nav_model.remove(child_id); + } else { + break; + } + } + } + Command::none() + } + ProjectNode::File { path, .. } => { + //TODO: go to already open file if possible + self.open_tab(Some(path.clone())); + self.update_title() + } + } + } + + fn update(&mut self, message: Message) -> Command { match message { Message::New => { self.open_tab(None); @@ -181,7 +326,6 @@ impl cosmic::Application for App { return Command::perform( async { if let Some(handle) = rfd::AsyncFileDialog::new().pick_file().await { - println!("{}", handle.path().display()); message::app(Message::Open(handle.path().to_owned())) } else { message::none() @@ -311,19 +455,7 @@ impl cosmic::Application for App { } }; - let mut project_row = widget::row::with_capacity(2); - if !self.projects.is_empty() { - /*TODO: project tree view - let mut project_list = widget::column::with_capacity(self.projects.len()); - for project in self.projects.iter() { - project_list = project_list.push(widget::text(&project.name)); - } - project_row = project_row.push(project_list); - */ - } - project_row = project_row.push(tab_column); - - let content: Element<_> = project_row.into(); + let content: Element<_> = tab_column.into(); // Uncomment to debug layout: //content.explain(cosmic::iced::Color::WHITE) diff --git a/src/project.rs b/src/project.rs index 07b13e0..b95900e 100644 --- a/src/project.rs +++ b/src/project.rs @@ -1,28 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-only + use std::{ fs, io, path::{Path, PathBuf}, }; -pub struct Project { - path: PathBuf, - name: String, +#[derive(Clone, Debug)] +pub enum ProjectNode { + Root { + name: String, + path: PathBuf, + open: bool, + }, + Folder { + name: String, + path: PathBuf, + open: bool, + }, + File { + name: String, + path: PathBuf, + }, } -impl Project { +impl ProjectNode { pub fn new>(path: P) -> io::Result { let path = fs::canonicalize(path)?; let name = path .file_name() .ok_or(io::Error::new( io::ErrorKind::Other, - format!("Path {:?} has no file name", path), + format!("path {:?} has no file name", path), ))? .to_str() .ok_or(io::Error::new( io::ErrorKind::Other, - format!("Path {:?} is not valid UTF-8", path), + format!("path {:?} is not valid UTF-8", path), ))? .to_string(); - Ok(Self { path, name }) + Ok(if path.is_dir() { + Self::Folder { + path, + name, + open: false, + } + } else { + Self::File { path, name } + }) + } + + pub fn icon_name(&self) -> &str { + match self { + //TODO: different icon for project? + ProjectNode::Root { open, .. } => { + if *open { + "go-down-symbolic" + } else { + "go-next-symbolic" + } + } + ProjectNode::Folder { open, .. } => { + if *open { + "go-down-symbolic" + } else { + "go-next-symbolic" + } + } + ProjectNode::File { .. } => "text-x-generic", + } + } + + pub fn name(&self) -> &str { + match self { + ProjectNode::Root { name, .. } => name, + ProjectNode::Folder { name, .. } => name, + ProjectNode::File { name, .. } => name, + } } }