fractal/components/pill/
search_entry.rsuse adw::{prelude::*, subclass::prelude::*};
use gtk::{
glib,
glib::{clone, closure_local},
CompositeTemplate,
};
use crate::{
components::{Pill, PillSource},
prelude::*,
};
mod imp {
use std::{cell::RefCell, collections::HashMap, marker::PhantomData, sync::LazyLock};
use glib::subclass::{InitializingObject, Signal};
use super::*;
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/components/pill/search_entry.ui")]
#[properties(wrapper_type = super::PillSearchEntry)]
pub struct PillSearchEntry {
#[template_child]
pub text_view: TemplateChild<gtk::TextView>,
#[template_child]
pub text_buffer: TemplateChild<gtk::TextBuffer>,
#[property(get = Self::text)]
text: PhantomData<glib::GString>,
#[property(get = Self::editable, set = Self::set_editable, explicit_notify)]
editable: PhantomData<bool>,
pub pills: RefCell<HashMap<String, gtk::TextChildAnchor>>,
}
#[glib::object_subclass]
impl ObjectSubclass for PillSearchEntry {
const NAME: &'static str = "PillSearchEntry";
type Type = super::PillSearchEntry;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for PillSearchEntry {
fn signals() -> &'static [Signal] {
static SIGNALS: LazyLock<Vec<Signal>> = LazyLock::new(|| {
vec![Signal::builder("pill-removed")
.param_types([PillSource::static_type()])
.build()]
});
SIGNALS.as_ref()
}
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
self.text_buffer.connect_delete_range(clone!(
#[weak]
obj,
move |_, start, end| {
if start == end {
return;
}
let mut current = *start;
loop {
if let Some(source) = current
.child_anchor()
.and_then(|a| a.widgets().first().cloned())
.and_downcast_ref::<Pill>()
.and_then(Pill::source)
{
let removed = obj
.imp()
.pills
.borrow_mut()
.remove(&source.identifier())
.is_some();
if removed {
obj.emit_by_name::<()>("pill-removed", &[&source]);
}
}
current.forward_char();
if ¤t == end {
break;
}
}
}
));
self.text_buffer
.connect_insert_text(|text_buffer, location, text| {
let mut changed = false;
loop {
if location.child_anchor().is_some() {
changed = true;
if !location.forward_char() {
break;
}
} else {
break;
}
}
if changed {
text_buffer.place_cursor(location);
text_buffer.stop_signal_emission_by_name("insert-text");
text_buffer.insert(location, text);
}
});
self.text_buffer.connect_text_notify(clone!(
#[weak]
obj,
move |_| {
obj.notify_text();
}
));
}
}
impl WidgetImpl for PillSearchEntry {}
impl BinImpl for PillSearchEntry {}
impl PillSearchEntry {
fn text(&self) -> glib::GString {
let (start, end) = self.text_buffer.bounds();
self.text_buffer.text(&start, &end, false)
}
fn editable(&self) -> bool {
self.text_view.is_editable()
}
fn set_editable(&self, editable: bool) {
if self.editable() == editable {
return;
}
self.text_view.set_editable(editable);
self.obj().notify_editable();
}
}
}
glib::wrapper! {
pub struct PillSearchEntry(ObjectSubclass<imp::PillSearchEntry>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
impl PillSearchEntry {
pub fn new() -> Self {
glib::Object::new()
}
pub fn add_pill(&self, source: &impl IsA<PillSource>) {
let imp = self.imp();
let identifier = source.identifier();
if imp.pills.borrow().contains_key(&identifier) {
return;
}
let pill = Pill::new(source);
pill.set_margin_start(3);
pill.set_margin_end(3);
let (mut start_iter, mut end_iter) = imp.text_buffer.bounds();
loop {
if start_iter.child_anchor().is_some() {
start_iter.forward_char();
} else {
break;
}
}
imp.text_buffer.delete(&mut start_iter, &mut end_iter);
let anchor = imp.text_buffer.create_child_anchor(&mut start_iter);
imp.text_view.add_child_at_anchor(&pill, &anchor);
imp.pills.borrow_mut().insert(identifier, anchor);
imp.text_view.grab_focus();
}
pub fn remove_pill(&self, identifier: &str) {
let imp = self.imp();
let Some(anchor) = imp.pills.borrow_mut().remove(identifier) else {
return;
};
if anchor.is_deleted() {
return;
}
let text_buffer = &self.imp().text_buffer;
let mut start_iter = text_buffer.iter_at_child_anchor(&anchor);
let mut end_iter = start_iter;
end_iter.forward_char();
text_buffer.delete(&mut start_iter, &mut end_iter);
}
pub fn clear(&self) {
let text_buffer = &self.imp().text_buffer;
let (mut start, mut end) = text_buffer.bounds();
text_buffer.delete(&mut start, &mut end);
}
pub fn connect_pill_removed<F: Fn(&Self, PillSource) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_closure(
"pill-removed",
true,
closure_local!(|obj: Self, source: PillSource| {
f(&obj, source);
}),
)
}
}