From 08be3cb6130e4716e6d8216616d5a07c91ff0447 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sat, 30 Sep 2023 00:22:33 +0200 Subject: [PATCH] Add custom search bar with tags --- data/res/style.css | 11 ++- data/ui/home_page.blp | 9 +-- data/ui/search_entry.blp | 31 ++++++++ data/ui/search_tag.blp | 21 ++++++ po/POTFILES | 2 + src/home_page.rs | 13 ++-- src/main.rs | 2 + src/search_entry.rs | 150 +++++++++++++++++++++++++++++++++++++++ src/search_tag.rs | 60 ++++++++++++++++ 9 files changed, 283 insertions(+), 16 deletions(-) create mode 100644 data/ui/search_entry.blp create mode 100644 data/ui/search_tag.blp create mode 100644 src/search_entry.rs create mode 100644 src/search_tag.rs diff --git a/data/res/style.css b/data/res/style.css index 82485bf..a4345b5 100644 --- a/data/res/style.css +++ b/data/res/style.css @@ -1,3 +1,10 @@ -.searchbar { - padding: 6px 6px 7px 6px; +.searchbar .searchtag { + background-color: alpha(currentColor, 0.1); + border-radius: 100px; +} + +.searchbar .searchtag > button { + min-width: 24px; + min-height: 24px; + margin: 0px; } \ No newline at end of file diff --git a/data/ui/home_page.blp b/data/ui/home_page.blp index 038786b..48aafe0 100644 --- a/data/ui/home_page.blp +++ b/data/ui/home_page.blp @@ -21,13 +21,8 @@ template $MusicusHomePage : Adw.NavigationPage { maximum-size: 1000; tightening-threshold: 600; - Adw.Bin { - styles ["searchbar"] - - Gtk.SearchEntry search_entry { - placeholder-text: _("Enter composers, performers, works…"); - search-changed => $search() swapped; - } + $MusicusSearchEntry search_entry { + activate => $select() swapped; } } diff --git a/data/ui/search_entry.blp b/data/ui/search_entry.blp new file mode 100644 index 0000000..1da87f8 --- /dev/null +++ b/data/ui/search_entry.blp @@ -0,0 +1,31 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusSearchEntry : Gtk.Box { + styles ["searchbar"] + + margin-start: 12; + margin-end: 12; + margin-top: 6; + margin-bottom: 6; + + Gtk.Image { + icon-name: "system-search-symbolic"; + } + + Gtk.Box tags_box { + valign: center; + } + + Gtk.Text text { + placeholder-text: _("Enter composers, performers, works…"); + hexpand: true; + activate => $activate() swapped; + backspace => $backspace() swapped; + } + + Gtk.Image clear_icon { + icon-name: "edit-clear-symbolic"; + tooltip-text: _("Clear entry"); + } +} \ No newline at end of file diff --git a/data/ui/search_tag.blp b/data/ui/search_tag.blp new file mode 100644 index 0000000..a7d33eb --- /dev/null +++ b/data/ui/search_tag.blp @@ -0,0 +1,21 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusSearchTag : Gtk.Box { + styles ["searchtag"] + + margin-start: 6; + margin-end: 6; + + Gtk.Label label { + styles ["caption-heading"] + margin-start: 12; + margin-end: 6; + } + + Gtk.Button button { + styles ["flat", "circular"] + icon-name: "window-close-symbolic"; + clicked => $remove() swapped; + } +} \ No newline at end of file diff --git a/po/POTFILES b/po/POTFILES index 3c2699c..a9ea4dd 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -1,5 +1,7 @@ data/res/home_page.blp data/res/playlist_page.blp +data/ui/search_entry.blp +data/ui/search_tag.blp data/res/tile.blp data/res/welcome_page.blp data/res/window.blp diff --git a/src/home_page.rs b/src/home_page.rs index 05ff254..90299fe 100644 --- a/src/home_page.rs +++ b/src/home_page.rs @@ -1,10 +1,9 @@ -use crate::player::MusicusPlayer; +use crate::{player::MusicusPlayer, tile::MusicusTile, search_entry::MusicusSearchEntry}; use adw::subclass::{navigation_page::NavigationPageImpl, prelude::*}; use gtk::{glib, glib::Properties, prelude::*}; use std::cell::RefCell; mod imp { - use crate::tile::MusicusTile; use super::*; @@ -16,7 +15,7 @@ mod imp { pub player: RefCell, #[template_child] - pub search_entry: TemplateChild, + pub search_entry: TemplateChild, #[template_child] pub persons_flow_box: TemplateChild, #[template_child] @@ -47,8 +46,8 @@ mod imp { impl ObjectImpl for MusicusHomePage { fn constructed(&self) { self.parent_constructed(); - self.search_entry - .set_key_capture_widget(Some(self.obj().as_ref())); + + self.search_entry.set_key_capture_widget(&*self.obj()); self.player .borrow() @@ -87,7 +86,7 @@ impl MusicusHomePage { } #[template_callback] - fn search(&self, entry: >k::SearchEntry) { - log::info!("Search changed: \"{}\"", entry.text()); + fn select(&self, search_entry: &MusicusSearchEntry) { + search_entry.add_tag("Tag"); } } diff --git a/src/main.rs b/src/main.rs index 33f3869..046a811 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,8 @@ mod config; mod home_page; mod player; mod playlist_page; +mod search_entry; +mod search_tag; mod tile; mod welcome_page; mod window; diff --git a/src/search_entry.rs b/src/search_entry.rs new file mode 100644 index 0000000..88cab90 --- /dev/null +++ b/src/search_entry.rs @@ -0,0 +1,150 @@ +use crate::search_tag::MusicusSearchTag; +use adw::{gdk, glib, glib::clone, glib::subclass::Signal, prelude::*, subclass::prelude::*}; +use once_cell::sync::Lazy; +use std::cell::RefCell; + +mod imp { + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + #[template(file = "data/ui/search_entry.blp")] + pub struct MusicusSearchEntry { + #[template_child] + pub tags_box: TemplateChild, + #[template_child] + pub text: TemplateChild, + #[template_child] + pub clear_icon: TemplateChild, + + pub tags: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for MusicusSearchEntry { + const NAME: &'static str = "MusicusSearchEntry"; + type Type = super::MusicusSearchEntry; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.bind_template_instance_callbacks(); + klass.set_css_name("entry"); + + klass.add_shortcut( + >k::Shortcut::builder() + .trigger(>k::KeyvalTrigger::new( + gdk::Key::Escape, + gdk::ModifierType::empty(), + )) + .action(>k::CallbackAction::new(|widget, _| match widget + .downcast_ref::( + ) { + Some(obj) => { + obj.reset(); + true + } + None => false, + })) + .build(), + ); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for MusicusSearchEntry { + fn constructed(&self) { + let controller = gtk::GestureClick::new(); + + controller.connect_pressed(|gesture, _, _, _| { + gesture.set_state(gtk::EventSequenceState::Claimed); + }); + + controller.connect_released(clone!(@weak self as _self => move |_, _, _, _| { + _self.obj().reset(); + })); + + self.clear_icon.add_controller(controller); + } + + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = + Lazy::new(|| vec![Signal::builder("activate").build()]); + + SIGNALS.as_ref() + } + } + + impl WidgetImpl for MusicusSearchEntry { + fn grab_focus(&self) -> bool { + self.text.grab_focus_without_selecting() + } + } + + impl BoxImpl for MusicusSearchEntry {} +} + +glib::wrapper! { + pub struct MusicusSearchEntry(ObjectSubclass) + @extends gtk::Widget; +} + +#[gtk::template_callbacks] +impl MusicusSearchEntry { + pub fn new() -> Self { + glib::Object::new() + } + + pub fn set_key_capture_widget(&self, widget: &impl IsA) { + let controller = gtk::EventControllerKey::new(); + + controller.connect_key_pressed(clone!(@weak self as _self => @default-return glib::Propagation::Proceed, move |controller, _, _, _| { + match controller.forward(&_self.imp().text.get()) { + true => { + _self.grab_focus(); + glib::Propagation::Stop + }, + false => glib::Propagation::Proceed, + } + })); + + controller.connect_key_released(clone!(@weak self as _self => move |controller, _, _, _| { + controller.forward(&_self.imp().text.get()); + })); + + widget.add_controller(controller); + } + + pub fn reset(&self) { + let mut tags = self.imp().tags.borrow_mut(); + + while let Some(tag) = tags.pop() { + self.imp().tags_box.remove(&tag); + } + + self.imp().text.set_text(""); + } + + pub fn add_tag(&self, name: &str) { + self.imp().text.set_text(""); + let tag = MusicusSearchTag::new(name); + self.imp().tags_box.append(&tag); + self.imp().tags.borrow_mut().push(tag); + } + + #[template_callback] + fn activate(&self, _: >k::Text) { + self.emit_by_name::<()>("activate", &[]); + } + + #[template_callback] + fn backspace(&self, text: >k::Text) { + if text.cursor_position() == 0 { + if let Some(tag) = self.imp().tags.borrow_mut().pop() { + self.imp().tags_box.remove(&tag); + } + } + } +} diff --git a/src/search_tag.rs b/src/search_tag.rs new file mode 100644 index 0000000..5d716d6 --- /dev/null +++ b/src/search_tag.rs @@ -0,0 +1,60 @@ +use adw::{glib, glib::subclass::Signal, prelude::*, subclass::prelude::*}; +use once_cell::sync::Lazy; + +mod imp { + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + #[template(file = "data/ui/search_tag.blp")] + pub struct MusicusSearchTag { + #[template_child] + pub label: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for MusicusSearchTag { + const NAME: &'static str = "MusicusSearchTag"; + type Type = super::MusicusSearchTag; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.bind_template_instance_callbacks(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for MusicusSearchTag { + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = + Lazy::new(|| vec![Signal::builder("remove").build()]); + + SIGNALS.as_ref() + } + } + + impl WidgetImpl for MusicusSearchTag {} + impl BoxImpl for MusicusSearchTag {} +} + +glib::wrapper! { + pub struct MusicusSearchTag(ObjectSubclass) + @extends gtk::Widget; +} + +#[gtk::template_callbacks] +impl MusicusSearchTag { + pub fn new(label: &str) -> Self { + let tag: MusicusSearchTag = glib::Object::new(); + tag.imp().label.set_label(label); + tag + } + + #[template_callback] + fn remove(&self, _: >k::Button) { + self.emit_by_name::<()>("remove", &[]); + } +}