draft suggestion autocomplete feature

This commit is contained in:
yggverse 2025-03-10 17:22:21 +02:00
parent 1c4bde4004
commit 5b0de227c0
7 changed files with 201 additions and 4 deletions

View File

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

View File

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

View File

@ -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::<Item>();
Self {
popover: {
let p = Popover::builder()
.autohide(false)
.can_focus(false)
.halign(gtk::Align::Start)
.child(
&gtk::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::<ListItem>().unwrap().set_child(
Some(
&ActionRow::builder()
.use_markup(true)
.use_underline(true)
.build(),
),
)
});
f.connect_bind(|_, this| {
let l = this.downcast_ref::<ListItem>().unwrap();
let i = l.item().and_downcast::<Item>().unwrap();
let r = l.child().and_downcast::<ActionRow>().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, &gtk::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<usize>) {
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!("<b>{query}</b>")),
item.request.clone(),
item.request.clone(),
)); // @TODO
}
self.popover.popup();
return;
}
}
self.popover.popdown();
}
}

View File

@ -0,0 +1,19 @@
mod imp;
use gtk::glib::{self, Object};
glib::wrapper! {
pub struct Item(ObjectSubclass<imp::Item>);
}
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()
}
}

View File

@ -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<String>,
#[property(get, set)]
subtitle: RefCell<String>,
#[property(get, set)]
request: RefCell<String>,
}
#[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();
}
}

View File

@ -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<usize>) -> Vec<Item> {
self.memory.borrow_mut().contains_request(request, limit)
}
/// Get recent Items vector from `memory`, sorted by `ID` DESC

View File

@ -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<usize>) -> Vec<Item> {
let mut items: Vec<Item> = 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<usize>) -> Vec<Item> {
let mut recent: Vec<Item> = Vec::new();