diff --git a/src/app/browser/window/tab/item/page/navigation/bookmark.rs b/src/app/browser/window/tab/item/page/navigation/bookmark.rs index 37838b30..86c56842 100644 --- a/src/app/browser/window/tab/item/page/navigation/bookmark.rs +++ b/src/app/browser/window/tab/item/page/navigation/bookmark.rs @@ -48,7 +48,7 @@ impl Bookmark for Button { } fn update(&self, profile: &Profile, request: &Entry) { - let has_bookmark = profile.bookmark.contains_request(&request.text()); + let has_bookmark = profile.bookmark.is_match_request(&request.text()); self.set_icon_name(icon_name(has_bookmark)); self.set_tooltip_text(Some(tooltip_text(has_bookmark))); } diff --git a/src/app/browser/window/tab/item/page/navigation/request.rs b/src/app/browser/window/tab/item/page/navigation/request.rs index 40c15502..85f5915c 100644 --- a/src/app/browser/window/tab/item/page/navigation/request.rs +++ b/src/app/browser/window/tab/item/page/navigation/request.rs @@ -2,6 +2,7 @@ mod database; mod identity; mod primary_icon; mod search; +mod suggestion; use super::{ItemAction, Profile}; use adw::{prelude::AdwDialogExt, AlertDialog}; @@ -14,6 +15,7 @@ use gtk::{ use primary_icon::PrimaryIcon; use sqlite::Transaction; use std::{cell::Cell, rc::Rc}; +use suggestion::Suggestion; const PREFIX_DOWNLOAD: &str = "download:"; const PREFIX_SOURCE: &str = "source:"; @@ -79,6 +81,9 @@ impl Request for Entry { // Detect primary icon on construct entry.update_primary_icon(profile); + // Init additional features + let suggestion = Suggestion::build(&entry); + // Connect events entry.connect_icon_release({ let profile = profile.clone(); @@ -108,6 +113,11 @@ impl Request for Entry { // Update icons this.update_primary_icon(&profile); this.update_secondary_icon(); + + // Show search suggestions + if this.focus_child().is_some() { + suggestion.update(&profile, this, None); + } } }); diff --git a/src/app/browser/window/tab/item/page/navigation/request/suggestion.rs b/src/app/browser/window/tab/item/page/navigation/request/suggestion.rs new file mode 100644 index 00000000..5137fddf --- /dev/null +++ b/src/app/browser/window/tab/item/page/navigation/request/suggestion.rs @@ -0,0 +1,118 @@ +mod item; + +use adw::{ + prelude::{ActionRowExt, PopoverExt, PreferencesRowExt}, + ActionRow, +}; +use gtk::{ + gio::{ + prelude::{Cast, CastNone}, + ListStore, + }, + prelude::{EntryExt, ListItemExt, WidgetExt}, + Entry, ListItem, ListView, Popover, SignalListItemFactory, SingleSelection, +}; +pub use item::Item; + +pub struct Suggestion { + list_store: ListStore, + pub popover: Popover, +} + +impl Suggestion { + // Constructors + + /// Create new `Self` + pub fn build(request: &Entry) -> Self { + let list_store = ListStore::new::(); + Self { + popover: { + let p = Popover::builder() + .autohide(false) + .can_focus(false) + .halign(gtk::Align::Start) + .child( + >k::ScrolledWindow::builder() + //.css_classes(["view"]) + .child( + &ListView::builder() + .model( + &SingleSelection::builder() + .model(&list_store) + .autoselect(false) + .build(), + ) + .factory(&{ + let f = SignalListItemFactory::new(); + f.connect_setup(|_, this| { + this.downcast_ref::().unwrap().set_child( + Some( + &ActionRow::builder() + .use_markup(true) + .use_underline(true) + .build(), + ), + ) + }); + f.connect_bind(|_, this| { + let l = this.downcast_ref::().unwrap(); + let i = l.item().and_downcast::().unwrap(); + let r = l.child().and_downcast::().unwrap(); + r.set_title(&i.title()); + r.set_subtitle(&i.subtitle()); + }); + f + }) + .build(), + ) + .max_content_height(400) + .hscrollbar_policy(gtk::PolicyType::Never) + .propagate_natural_height(true) + .propagate_natural_width(true) + .build(), + ) + .has_arrow(false) + .build(); + p.set_parent(request); + p.set_offset( + request + .compute_point(request, >k::graphene::Point::zero()) + .unwrap() + .x() as i32, + 6, + ); + p.connect_realize({ + let request = request.clone(); + move |this| this.set_width_request(request.width()) + }); + p + }, + list_store, + } + } + + pub fn update(&self, profile: &super::Profile, request: &Entry, limit: Option) { + use gtk::prelude::EditableExt; + use itertools::Itertools; + if request.text_length() > 0 { + self.list_store.remove_all(); + let query = request.text(); + let items = profile.bookmark.contains_request(&query, limit); + if !items.is_empty() { + for item in items + .into_iter() + .sorted_by(|a, b| Ord::cmp(&b.request, &a.request)) + { + self.list_store.append(&Item::build( + item.request.replace(&*query, &format!("{query}")), + item.request.clone(), + item.request.clone(), + )); // @TODO + } + self.popover.popup(); + return; + } + } + self.popover.popdown(); + } +} diff --git a/src/app/browser/window/tab/item/page/navigation/request/suggestion/item.rs b/src/app/browser/window/tab/item/page/navigation/request/suggestion/item.rs new file mode 100644 index 00000000..76682748 --- /dev/null +++ b/src/app/browser/window/tab/item/page/navigation/request/suggestion/item.rs @@ -0,0 +1,19 @@ +mod imp; + +use gtk::glib::{self, Object}; + +glib::wrapper! { + pub struct Item(ObjectSubclass); +} + +impl Item { + // Constructors + + pub fn build(title: String, subtitle: String, request: String) -> Self { + Object::builder() + .property("title", title) + .property("subtitle", subtitle) + .property("request", request) + .build() + } +} diff --git a/src/app/browser/window/tab/item/page/navigation/request/suggestion/item/imp.rs b/src/app/browser/window/tab/item/page/navigation/request/suggestion/item/imp.rs new file mode 100644 index 00000000..8ce38354 --- /dev/null +++ b/src/app/browser/window/tab/item/page/navigation/request/suggestion/item/imp.rs @@ -0,0 +1,31 @@ +use gtk::{ + gio::subclass::prelude::{DerivedObjectProperties, ObjectImpl, ObjectImplExt, ObjectSubclass}, + glib::{self, Object, Properties}, + prelude::ObjectExt, +}; +use std::cell::RefCell; + +#[derive(Properties, Default)] +#[properties(wrapper_type = super::Item)] +pub struct Item { + #[property(get, set)] + title: RefCell, + #[property(get, set)] + subtitle: RefCell, + #[property(get, set)] + request: RefCell, +} + +#[glib::object_subclass] +impl ObjectSubclass for Item { + const NAME: &'static str = "SuggestionItem"; // @TODO make globally unique + type Type = super::Item; + type ParentType = Object; +} + +#[glib::derived_properties] +impl ObjectImpl for Item { + fn constructed(&self) { + self.parent_constructed(); + } +} diff --git a/src/profile/bookmark.rs b/src/profile/bookmark.rs index 632e4073..2ca95c26 100644 --- a/src/profile/bookmark.rs +++ b/src/profile/bookmark.rs @@ -63,8 +63,13 @@ impl Bookmark { // Getters /// Check `request` exists in the memory index - pub fn contains_request(&self, request: &str) -> bool { - self.memory.borrow_mut().contains_request(request) + pub fn is_match_request(&self, request: &str) -> bool { + self.memory.borrow_mut().is_match_request(request) + } + + /// Find Items match `request` + pub fn contains_request(&self, request: &str, limit: Option) -> Vec { + self.memory.borrow_mut().contains_request(request, limit) } /// Get recent Items vector from `memory`, sorted by `ID` DESC diff --git a/src/profile/bookmark/memory.rs b/src/profile/bookmark/memory.rs index c1a4304b..fbf2deef 100644 --- a/src/profile/bookmark/memory.rs +++ b/src/profile/bookmark/memory.rs @@ -30,7 +30,7 @@ impl Memory { } /// Check `request` exists in the memory index - pub fn contains_request(&self, request: &str) -> bool { + pub fn is_match_request(&self, request: &str) -> bool { for item in self.0.iter() { if item.request == request { return true; @@ -39,6 +39,20 @@ impl Memory { false } + /// Get Items match `request` + pub fn contains_request(&self, request: &str, limit: Option) -> Vec { + let mut items: Vec = Vec::new(); + for (i, item) in self.0.iter().enumerate() { + if limit.is_some_and(|l| i > l) { + break; + } + if item.request.contains(request) { + items.push(item.clone()) + } + } + items + } + /// Get recent Items vector sorted by `ID` DESC pub fn recent(&self, limit: Option) -> Vec { let mut recent: Vec = Vec::new();