diff options
| author | Nathan Reiner <nathan@nathanreiner.xyz> | 2023-07-26 21:19:59 +0200 |
|---|---|---|
| committer | Nathan Reiner <nathan@nathanreiner.xyz> | 2023-07-26 21:19:59 +0200 |
| commit | 61ac9375b4a35878576ac2727c5210cd9fc51a92 (patch) | |
| tree | 5e147ad0c60b35a83baab3f84143d3c40a09cdcd | |
| parent | 7679fcc3a0c4fadea00a1a320938851b1518028d (diff) | |
add gtk3 gui
| -rw-r--r-- | src/gui/generate.rs | 154 | ||||
| -rw-r--r-- | src/gui/icon.svg | 67 | ||||
| -rw-r--r-- | src/gui/load.rs | 63 | ||||
| -rw-r--r-- | src/gui/mod.rs | 22 | ||||
| -rw-r--r-- | src/gui/search.rs | 133 | ||||
| -rw-r--r-- | src/gui/state.rs | 38 | ||||
| -rw-r--r-- | src/gui/style.css | 18 | ||||
| -rw-r--r-- | src/gui/welcome.rs | 51 | ||||
| -rw-r--r-- | src/index.rs | 21 | ||||
| -rw-r--r-- | src/main.rs | 5 |
10 files changed, 544 insertions, 28 deletions
diff --git a/src/gui/generate.rs b/src/gui/generate.rs index 9fe9e3f..a7eb9e5 100644 --- a/src/gui/generate.rs +++ b/src/gui/generate.rs @@ -1,6 +1,13 @@ use gtk::prelude::*; +use gtk::glib; use super::state::{View, ViewManager}; +use std::sync::Arc; +use std::sync::Mutex; use std::rc::Rc; +use std::thread; + +use crate::index::Index; +use crate::index::GenState; pub struct Generate { vm : Rc<ViewManager> @@ -15,16 +22,149 @@ impl View for Generate { self.vm = vm } - fn make_current(&self) -> gtk::Box { - let center = gtk::Box::new(gtk::Orientation::Vertical, 10); - let main = gtk::Box::new(gtk::Orientation::Horizontal, 10); - let label = gtk::Label::new(Some("Generate")); + fn make_current(&self) -> Option<gtk::Box> { + let target_dir = Arc::new(Mutex::new(String::new())); + let index_file = Arc::new(Mutex::new(String::new())); + let (index_tx, index_rx) = std::sync::mpsc::channel(); + let load_next = Rc::new(Mutex::new(false)); + + let splash = gtk::Window::new(gtk::WindowType::Popup); + splash.set_type_hint(gtk::gdk::WindowTypeHint::Splashscreen); + splash.set_decorated(false); + splash.set_position(gtk::WindowPosition::Center); + splash.set_resizable(false); + splash.set_size_request(600, 200); + + let main = gtk::Box::new(gtk::Orientation::Vertical, 10); + main.set_widget_name("generate"); + let title = gtk::Label::new(Some("Generate Index")); + title.set_widget_name("title"); + + let step = gtk::Label::new(Some("Choose A Target Directory")); + step.set_halign(gtk::Align::Start); + let btn = gtk::Button::with_label("Choose"); + btn.set_sensitive(false); + let header = gtk::Box::new(gtk::Orientation::Horizontal, 0); + + let pick = gtk::FileChooserWidget::builder() + .create_folders(false) + .action(gtk::FileChooserAction::SelectFolder) + .build(); + + header.pack_start(&step, true, true, 10); + header.pack_start(&btn, false, false, 10); + + main.pack_start(&title, false, false, 0); + main.pack_start(&header, false, false, 0); + main.pack_start(&pick, true, true, 0); + + splash.add(&main); + + pick.connect_selection_changed(glib::clone!(@weak btn => move |pick| { + if pick.file().is_some() || pick.current_name().is_some() { + btn.set_sensitive(true); + } else { + btn.set_sensitive(false); + } + })); + + { + let target_dir = Arc::clone(&target_dir); + let index_file = Arc::clone(&index_file); + let load_next = Rc::clone(&load_next); + btn.connect_clicked(glib::clone!( + @weak splash, + @weak header, + @weak step, + @weak pick, + @weak main => move |btn| { + let td = { target_dir.lock().unwrap().clone() }; + + if td.is_empty() { + let uri = pick.uri().unwrap(); + let path = uri.as_str().trim_start_matches("file://"); + let mut target_dir = target_dir.lock().unwrap(); + *target_dir = path.to_string(); + step.set_text("Create A Index File"); + btn.set_label("Create"); + pick.set_action(gtk::FileChooserAction::Save); + } else { + let mut path = pick.current_folder().unwrap().to_str().unwrap().to_string(); + path += "/"; + path += pick.current_name().unwrap().to_string().as_str(); + *index_file.lock().unwrap() = path; + + main.remove(&header); + main.remove(&pick); + + let progress = gtk::ProgressBar::builder() + .ellipsize(gtk::pango::EllipsizeMode::Middle) + .show_text(true) + .text("Generating") + .build(); + + progress.set_margin(20); + main.pack_start(&progress, true, false, 0); + + let (tx, rx) = glib::MainContext::channel(glib::Priority::default()); + { + let target_dir = Arc::clone(&target_dir); + let index_file = Arc::clone(&index_file); + let index_tx = index_tx.clone(); + thread::spawn(move || { + let path = target_dir.lock().unwrap().to_string(); + let idx = Index::generate(path.as_str(), |s, p| { + let text = match s { + GenState::Fetching => { "Fetching" } + GenState::Parsing => { "Parsing" } + GenState::Merging => { "Merging" } + }; + tx.send(Some((text, p))).ok(); + }); + tx.send(Some(("Saving", 100))).ok(); + idx.save(index_file.lock().unwrap().to_string()); + tx.send(None).ok(); + index_tx.send(idx) + }); + } + + let load_next = Rc::clone(&load_next); + rx.attach(None, glib::clone!( + @weak splash, + @weak progress, + @weak main => @default-return glib::Continue(false), move |value| match value { + Some((s, p)) => { + progress.pulse(); + progress.set_fraction(f64::from(p) / 100.0); + progress.set_text(Some(s)); + glib::Continue(true) + } + None => { + splash.close(); + *load_next.lock().unwrap() = true; + glib::Continue(false) + } + })); + + main.show_all(); + } + })); + } - main.pack_start(&label, false, false, 0); + let vm = Rc::clone(&self.vm); + splash.connect_hide(move |_| { + if *load_next.lock().unwrap() { + let idx = index_rx.recv().unwrap(); + vm.set_index(idx); + vm.set_current_view("search"); + } else if vm.get_current_view() == "generate" { + vm.set_current_view("welcome") + } + }); - center.pack_start(&main, true, false, 0); + splash.show_all(); - center + None } } diff --git a/src/gui/icon.svg b/src/gui/icon.svg new file mode 100644 index 0000000..0dd19c0 --- /dev/null +++ b/src/gui/icon.svg @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + width="48" + height="48" + viewBox="0 0 12.7 12.7" + version="1.1" + id="svg5" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + sodipodi:docname="icon.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview7" + pagecolor="#505050" + bordercolor="#eeeeee" + borderopacity="1" + inkscape:showpageshadow="0" + inkscape:pageopacity="0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#505050" + inkscape:document-units="mm" + showgrid="false" + inkscape:zoom="3.6183075" + inkscape:cx="34.961098" + inkscape:cy="19.622434" + inkscape:window-width="1894" + inkscape:window-height="1011" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="layer1" /> + <defs + id="defs2" /> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1"> + <circle + style="fill:#244c66;fill-opacity:1;stroke:none;stroke-width:0.758009;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="circle7365-3-6" + cx="6.3499999" + cy="6.3499999" + r="6.0858917" /> + <circle + style="fill:#407d6c;fill-opacity:1;stroke:none;stroke-width:0.605315;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="path4064-5" + cx="5.1240602" + cy="6.6535206" + r="4.859952" /> + <circle + style="fill:#7db290;fill-opacity:1;stroke:none;stroke-width:0.473211;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="path4064" + cx="4.6072431" + cy="7.651989" + r="3.7993114" /> + <circle + style="fill:#b5c7cc;fill-opacity:1;stroke:none;stroke-width:0.336748;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="circle7365" + cx="4.4472618" + cy="8.6169968" + r="2.7036855" /> + </g> +</svg> diff --git a/src/gui/load.rs b/src/gui/load.rs new file mode 100644 index 0000000..e075d0f --- /dev/null +++ b/src/gui/load.rs @@ -0,0 +1,63 @@ +use gtk::glib; +use gtk::prelude::*; +use super::state::{View, ViewManager}; +use std::rc::Rc; + +use crate::index::Index; + +pub struct Load { + vm : Rc<ViewManager> +} + +impl View for Load { + fn name(&self) -> &str { + "load" + } + + fn set_vm(&mut self, vm : Rc<ViewManager>) { + self.vm = vm + } + + fn make_current(&self) -> Option<gtk::Box> { + let vm = Rc::clone(&self.vm); + glib::MainContext::default().spawn_local(async move { + let dialog = gtk::FileChooserDialog::builder() + .action(gtk::FileChooserAction::Open) + .transient_for(vm.get_window()) + .title("Open Index File") + .build(); + + dialog.add_button("Choose", gtk::ResponseType::Accept); + + let _ = dialog.run_future().await; + dialog.close(); + dialog.hide(); + + if let Some(uri) = dialog.uri() { + let splash = gtk::Window::new(gtk::WindowType::Popup); + splash.set_type_hint(gtk::gdk::WindowTypeHint::Splashscreen); + splash.set_decorated(false); + splash.set_position(gtk::WindowPosition::Center); + splash.set_resizable(false); + splash.set_size_request(300, 200); + splash.add(>k::Label::new(Some("Loading"))); + splash.show_all(); + let path = uri.trim_start_matches("file://").to_string(); + let index = Index::from_file(&path); + vm.set_index(index); + vm.set_current_view("search"); + splash.close(); + } else { + vm.set_current_view("welcome"); + } + }); + + None + } +} + +impl Load { + pub fn new() -> Self { + Self { vm : Rc::new(ViewManager::empty()) } + } +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 68f8ab3..2701e5c 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -3,12 +3,15 @@ use std::rc::Rc; use gtk::prelude::*; use gtk::Application; +use gtk::gdk; use self::state::View; mod state; mod welcome; mod generate; +mod search; +mod load; pub fn run() { let app = Application::builder() @@ -16,6 +19,16 @@ pub fn run() { .build(); app.connect_activate(|app| { + let provider = gtk::CssProvider::new(); + let style = include_bytes!("style.css"); + provider.load_from_data(style).expect("failed to load default style"); + + gtk::StyleContext::add_provider_for_screen( + &gdk::Screen::default().expect("error initializing gtk css provider"), + &provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION + ); + let window = gtk::ApplicationWindow::builder() .application(app) .default_width(800) @@ -23,17 +36,24 @@ pub fn run() { .title("Index Search") .build(); + window.set_widget_name("mainwindow"); + let mut vm = state::ViewManager::new(window); let welcome = Rc::new(Mutex::new(welcome::Welcome::new())); vm.add_view(welcome.clone()); let generate = Rc::new(Mutex::new(generate::Generate::new())); vm.add_view(generate.clone()); + let load = Rc::new(Mutex::new(load::Load::new())); + vm.add_view(load.clone()); + let search = Rc::new(Mutex::new(search::Search::new())); + vm.add_view(search.clone()); let vm = Rc::new(vm); welcome.lock().unwrap().set_vm(Rc::clone(&vm)); generate.lock().unwrap().set_vm(Rc::clone(&vm)); + load.lock().unwrap().set_vm(Rc::clone(&vm)); + search.lock().unwrap().set_vm(Rc::clone(&vm)); vm.set_current_view("welcome"); - vm.get_window().show_all(); }); app.run(); diff --git a/src/gui/search.rs b/src/gui/search.rs new file mode 100644 index 0000000..a1c6ff3 --- /dev/null +++ b/src/gui/search.rs @@ -0,0 +1,133 @@ +use gtk::gdk::keys::constants::Return as RETURN; +use gtk::glib; +use gtk::prelude::*; +use super::state::{View, ViewManager}; +use std::rc::Rc; +use std::thread; + +pub struct Search { + vm : Rc<ViewManager> +} + +impl View for Search { + fn name(&self) -> &str { + "search" + } + + fn set_vm(&mut self, vm : Rc<ViewManager>) { + self.vm = vm + } + + fn make_current(&self) -> Option<gtk::Box> { + let main = gtk::Box::new(gtk::Orientation::Vertical, 0); + let scroll = gtk::ScrolledWindow::new(gtk::Adjustment::NONE, gtk::Adjustment::NONE); + let search = gtk::SearchEntry::new(); + let progress = gtk::ProgressBar::new(); + search.set_widget_name("search_entry"); + main.pack_start(&search, false, false, 0); + main.pack_start(&progress, false, false, 0); + let list = gtk::ListBox::new(); + scroll.add(&list); + + let empty_container = gtk::Box::new(gtk::Orientation::Vertical, 0); + + let bt = include_bytes!("icon.svg"); + let loader = gtk::gdk_pixbuf::PixbufLoader::new(); + loader.write(bt).ok(); + loader.set_size(200, 200); + loader.close().ok(); + let pixbuf = loader.pixbuf().unwrap(); + let image = gtk::Image::from_pixbuf(Some(&pixbuf)); + + empty_container.pack_start(&image, true, true, 0); + main.pack_start(&empty_container, true, true, 0); + + let vm = Rc::clone(&self.vm); + search.connect_key_press_event(glib::clone!( + @strong scroll, + @strong list, + @weak progress, + @weak main, + @weak empty_container => @default-return gtk::Inhibit(false), move |entry, event| { + match event.keyval() { + RETURN => { + let query = entry.text().to_string(); + + if query.is_empty() { + main.remove(&scroll); + main.remove(&empty_container); + main.pack_start(&empty_container, true, true, 0); + return gtk::Inhibit(false); + } + + let (tx, rx) = glib::MainContext::channel(glib::Priority::default()); + let (status_tx, status_rx) = glib::MainContext::channel(glib::Priority::default()); + + let index = vm.get_index(); + thread::spawn(move || { + let searchvec = crate::splitter::split_to_words(query); + let results = index.lock().unwrap().search(searchvec, |p| { + status_tx.send(p).ok(); + }); + tx.send(results).ok(); + }); + + status_rx.attach(None, glib::clone!( + @weak progress => @default-return glib::Continue(true), move |p| { + progress.set_fraction(f64::from(p) / 100.0); + + if p == 100 { + glib::Continue(false) + } else { + glib::Continue(true) + } + })); + + rx.attach(None, glib::clone!( + @weak progress, + @weak list, + @weak scroll => @default-return glib::Continue(false), move |results| { + main.remove(&scroll); + main.remove(&empty_container); + + progress.set_fraction(0.0); + + if results.is_empty() { + main.pack_start(&empty_container, true, true, 0); + return glib::Continue(false); + } else { + for child in list.children() { + list.remove(&child); + } + + for result in results.iter().rev().take(1000) { + let entry = gtk::Box::new(gtk::Orientation::Horizontal, 0); + entry.set_margin(10); + let path_label = gtk::Label::new(Some(&result.path)); + let prio_label = gtk::Label::new(Some(&result.priority.to_string())); + entry.pack_start(&prio_label, false, false, 10); + entry.pack_start(&path_label, true, true, 10); + list.prepend(&entry) + } + + main.pack_start(&scroll, true, true, 0); + } + main.show_all(); + + glib::Continue(false) + })); + } + _ => {} + } + gtk::Inhibit(false) + })); + + Some(main) + } +} + +impl Search { + pub fn new() -> Self { + Self { vm : Rc::new(ViewManager::empty()) } + } +} diff --git a/src/gui/state.rs b/src/gui/state.rs index 7b9542a..be41f86 100644 --- a/src/gui/state.rs +++ b/src/gui/state.rs @@ -1,27 +1,31 @@ use gtk::prelude::*; use std::collections::HashMap; -use std::rc::Rc; use std::sync::Mutex; +use crate::index::Index; +use std::rc::Rc; +use std::sync::Arc; pub trait View { fn name(&self) -> &str; fn set_vm(&mut self, vm : Rc<ViewManager>); - fn make_current(&self) -> gtk::Box; + fn make_current(&self) -> Option<gtk::Box>; } #[derive(Default)] pub struct ViewManager { views : HashMap<String, Rc<Mutex<dyn View>>>, - pub current : String, - pub window : Option<gtk::ApplicationWindow> + pub current : Mutex<String>, + pub window : Option<gtk::ApplicationWindow>, + index : Arc<Mutex<Index>> } impl ViewManager { pub fn new(window : gtk::ApplicationWindow) -> Self { Self { views : HashMap::new(), - current : String::new(), + current : Mutex::new(String::new()), window : Some(window), + index : Arc::new(Mutex::new(Index::default())) } } @@ -41,11 +45,31 @@ impl ViewManager { for child in window.children() { window.remove(&child) } - window.add(&b); - window.show_all() + { + *(self.current.lock().unwrap()) = name.to_string(); + } + if let Some(b) = b { + window.add(&b); + window.show_all(); + } else { + window.hide(); + } + } + + pub fn get_current_view(&self) -> String { + self.current.lock().unwrap().to_string() } pub fn get_window(&self) -> >k::ApplicationWindow { self.window.as_ref().unwrap() } + + pub fn get_index(&self) -> Arc<Mutex<Index>> { + Arc::clone(&self.index) + } + + pub fn set_index(&self, index : Index) { + let mut idx = self.index.lock().unwrap(); + *idx = index; + } } diff --git a/src/gui/style.css b/src/gui/style.css new file mode 100644 index 0000000..a25a28c --- /dev/null +++ b/src/gui/style.css @@ -0,0 +1,18 @@ +#generate #title { + font-size: 2em; + background-color: #282828; + padding: 20px; +} + +#search_entry { + background-color: #282828; + border-radius: 0px; + padding: 10px; + border: none; +} + + +#search_entry:focus { + border: none; + box-shadow: none; +} diff --git a/src/gui/welcome.rs b/src/gui/welcome.rs index 3dac0dd..7c7d02f 100644 --- a/src/gui/welcome.rs +++ b/src/gui/welcome.rs @@ -1,3 +1,4 @@ +use gtk::glib; use gtk::prelude::*; use super::state::{View, ViewManager}; use std::rc::Rc; @@ -15,25 +16,61 @@ impl View for Welcome { self.vm = vm } - fn make_current(&self) -> gtk::Box { + fn make_current(&self) -> Option<gtk::Box> { + let splash = gtk::Window::new(gtk::WindowType::Popup); + splash.set_type_hint(gtk::gdk::WindowTypeHint::Splashscreen); + splash.set_decorated(false); + splash.set_position(gtk::WindowPosition::Center); + splash.set_resizable(false); + splash.set_size_request(300, 200); let center = gtk::Box::new(gtk::Orientation::Vertical, 10); let main = gtk::Box::new(gtk::Orientation::Horizontal, 10); let gen_button = gtk::Button::with_label("Generate"); - let merge_button = gtk::Button::with_label("Merge"); let load_button = gtk::Button::with_label("Load"); - main.pack_start(&gen_button, true, false, 10); - main.pack_start(&merge_button, true, false, 10); - main.pack_start(&load_button, true, false, 10); + main.pack_start(&gen_button, true, true, 0); + main.pack_start(&load_button, true, true, 0); + main.set_height_request(50); + main.set_homogeneous(true); + + let bt = include_bytes!("icon.svg"); + let loader = gtk::gdk_pixbuf::PixbufLoader::new(); + loader.write(bt).ok(); + loader.set_size(100, 100); + loader.close().ok(); + let pixbuf = loader.pixbuf().unwrap(); + let image = gtk::Image::from_pixbuf(Some(&pixbuf)); + + center.pack_start(&image, true, true, 0); center.pack_start(&main, true, false, 0); + self.vm.get_window().set_type_hint(gtk::gdk::WindowTypeHint::Splashscreen); + let vm = Rc::clone(&self.vm); - gen_button.connect_clicked(move |_| { + gen_button.connect_clicked(glib::clone!(@weak splash => move |_| { vm.set_current_view("generate"); + splash.close(); + })); + + let vm = Rc::clone(&self.vm); + load_button.connect_clicked(glib::clone!(@weak splash => move |_| { + vm.set_current_view("load"); + splash.close(); + })); + + let vm = Rc::clone(&self.vm); + splash.connect_hide(move |_| { + if vm.get_current_view() == "welcome" { + vm.get_window().application().unwrap().quit(); + } }); - center + center.set_margin(10); + splash.add(¢er); + splash.show_all(); + + None } } diff --git a/src/index.rs b/src/index.rs index cc86f0c..be23e0e 100644 --- a/src/index.rs +++ b/src/index.rs @@ -16,7 +16,7 @@ use crate::vector; /// or read from a file. #[derive(Clone, Debug)] pub struct Index { - filecache : Vec<FileCache>, + pub filecache : Vec<FileCache>, } impl Default for Index { @@ -69,7 +69,7 @@ impl Index { if content.is_empty() { result_tx.send(FileCache { - path: "".to_string(), + path: "[is_empty]".to_string(), vector : FileVector::default() }).ok(); continue; @@ -78,7 +78,7 @@ impl Index { let words : Vec<String> = splitter::split_to_words(content); let fv = FileVector::from_words(words); result_tx.send(FileCache { - path: "".to_string(), + path: path.to_string(), vector : fv }).ok(); } @@ -128,7 +128,7 @@ impl Index { Self { filecache } } - pub fn search(&self, search_args : Vec<String>) -> Vec<SearchResult> { + pub fn search(&self, search_args : Vec<String>, callback : impl Fn(u8)) -> Vec<SearchResult> { let mut v : HashMap<Indexer, Count> = HashMap::new(); let mut opt : HashMap<Indexer, Count> = HashMap::new(); @@ -149,13 +149,20 @@ impl Index { let mut results : Vec<SearchResult> = Vec::new(); - for filecache in self.filecache.iter() { + let mut last_p = 0; + for (i, filecache) in self.filecache.iter().enumerate() { let mut r = SearchResult { priority : 0, path : filecache.path.clone() }; r.priority = vector::match_vector(&v, &filecache.vector); if r.priority > 0 { r.priority += vector::scalar_product(&opt, &filecache.vector); results.push(r); } + + let p = i * 100 / self.filecache.len(); + if last_p < p { + callback(p as u8); + last_p = p; + } } results.sort_by(|a, b| b.priority.cmp(&a.priority)); results @@ -170,4 +177,8 @@ impl Index { pub fn num_files(&self) -> usize { self.filecache.len() } + + pub fn import(&mut self, index : Index) { + self.filecache = index.filecache; + } } diff --git a/src/main.rs b/src/main.rs index 1a8a7c9..20697f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,7 +49,10 @@ fn main() { let searchvec = splitter::split_to_words(search); let idx = Index::from_file(&file); - let results = idx.search(searchvec); + let results = idx.search(searchvec, |p| { + eprint!("\r\x1b[2K{}% searched", p); + }); + eprint!("\r\x1b[2K"); for result in results { println!("{}", result.path); } |