init search on page feature once

This commit is contained in:
yggverse 2024-12-17 01:50:12 +02:00
parent 4b357f8229
commit f767c11789
23 changed files with 449 additions and 266 deletions

View file

@ -0,0 +1,25 @@
mod tag;
use tag::Tag;
use gtk::{prelude::TextBufferExt, TextBuffer};
pub struct Buffer {
pub text_buffer: TextBuffer,
pub tag: Tag,
}
impl Buffer {
// Constructors
/// Create new `Self`
pub fn new(text_buffer: TextBuffer) -> Self {
// Init components
// * create new tag objects required for new buffer,
// instead of re-use existing refs (maybe the bug)
let tag = Tag::new(text_buffer.tag_table());
// Init `Self`
Self { text_buffer, tag }
}
}

View file

@ -0,0 +1,32 @@
mod current;
mod found;
use gtk::{TextTag, TextTagTable};
pub struct Tag {
pub current: TextTag,
pub found: TextTag,
}
impl Tag {
// Constructors
/// Create new `Self`
pub fn new(table: TextTagTable) -> Self {
// Init components
let current = current::new();
let found = found::new();
// Init tag table
// keep order as `current` should overwrite `found` tag style
// https://docs.gtk.org/gtk4/method.TextTag.set_priority.html
for &tag in &[&current, &found] {
if !table.add(tag) {
todo!()
}
}
// Init `Self`
Self { current, found }
}
}

View file

@ -0,0 +1,8 @@
use gtk::{gdk::RGBA, TextTag};
pub fn new() -> TextTag {
TextTag::builder()
.background_rgba(&RGBA::new(0.0, 0.4, 0.9, 1.0)) // @TODO use accent colors after adw 1.6 update
.foreground_rgba(&RGBA::new(1.0, 1.0, 1.0, 1.0))
.build()
}

View file

@ -0,0 +1,7 @@
use gtk::{gdk::RGBA, TextTag};
pub fn new() -> TextTag {
TextTag::builder()
.background_rgba(&RGBA::new(0.5, 0.5, 0.5, 0.5)) // @TODO use accent colors after adw 1.6 update
.build()
}

View file

@ -0,0 +1,144 @@
mod close;
mod input;
mod match_case;
mod navigation;
use super::Buffer;
use input::Input;
use navigation::Navigation;
use gtk::{
prelude::{BoxExt, ButtonExt, CheckButtonExt, EditableExt, TextBufferExt, WidgetExt},
Align, Box, Orientation, TextIter, TextSearchFlags,
};
use std::{cell::RefCell, rc::Rc};
pub struct Form {
pub g_box: Box,
}
impl Form {
// Constructors
/// Create new `Self`
pub fn new(buffer: &Rc<RefCell<Option<Buffer>>>) -> Self {
// Init components
let close = close::new();
let input = Rc::new(Input::new());
let match_case = match_case::new();
let navigation = Rc::new(Navigation::new());
// Init main container
let g_box = Box::builder()
.orientation(Orientation::Horizontal)
.valign(Align::Center)
.vexpand(false)
.visible(false)
.build();
g_box.append(&input.entry);
g_box.append(&navigation.g_box);
g_box.append(&match_case);
g_box.append(&close);
// Connect events
close.connect_clicked({
let input = input.clone();
move |_| input.clean()
});
input.entry.connect_changed({
let input = input.clone();
let match_case = match_case.clone();
let navigation = navigation.clone();
let buffer = buffer.clone();
move |_| {
navigation.update(find(
&buffer,
input.entry.text().as_str(),
match_case.is_active(),
));
input.update(navigation.is_match());
}
});
match_case.connect_toggled({
let input = input.clone();
let navigation = navigation.clone();
let buffer = buffer.clone();
move |this| {
navigation.update(find(&buffer, input.entry.text().as_str(), this.is_active()));
input.update(navigation.is_match());
}
});
// Done
Self { g_box }
}
// Actions
pub fn show(&self) {
//self.buffer.get_mut().is_none()
self.g_box.set_visible(true)
}
pub fn hide(&self) {
self.g_box.set_visible(false)
}
pub fn toggle(&self) {
if self.g_box.is_visible() {
self.hide()
} else {
self.show()
}
}
}
// Tools
fn find(
buffer: &Rc<RefCell<Option<Buffer>>>,
subject: &str,
is_match_case: bool,
) -> Vec<(TextIter, TextIter)> {
// Init matches holder
let mut result = Vec::new();
// Borrow buffer
match buffer.borrow().as_ref() {
Some(buffer) => {
// Get iters
let buffer_start = buffer.text_buffer.start_iter();
let buffer_end = buffer.text_buffer.end_iter();
// Cleanup previous search highlights
buffer
.text_buffer
.remove_tag(&buffer.tag.current, &buffer_start, &buffer_end);
buffer
.text_buffer
.remove_tag(&buffer.tag.found, &buffer_start, &buffer_end);
// Begin new search
let mut next = buffer_start;
while let Some((match_start, match_end)) = next.forward_search(
subject,
match is_match_case {
true => TextSearchFlags::TEXT_ONLY,
false => TextSearchFlags::CASE_INSENSITIVE,
},
None, // unlimited
) {
buffer
.text_buffer
.apply_tag(&buffer.tag.found, &match_start, &match_end);
next = match_end;
result.push((match_start, match_end));
}
result
}
None => todo!(), // unexpected
}
}

View file

@ -0,0 +1,15 @@
use gtk::{gdk::Cursor, Align, Button};
const MARGIN: i32 = 6;
pub fn new() -> Button {
Button::builder()
.cursor(&Cursor::from_name("default", None).unwrap())
.icon_name("window-close-symbolic")
.margin_end(MARGIN)
.margin_start(MARGIN)
.valign(Align::Center)
.vexpand(false)
.tooltip_text("Close find bar")
.build()
}

View file

@ -0,0 +1,68 @@
use gtk::{
prelude::{EditableExt, EntryExt, WidgetExt},
Align, Entry, EntryIconPosition,
};
const MARGIN: i32 = 6;
pub struct Input {
pub entry: Entry,
}
impl Input {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
// Init widget
let entry = Entry::builder()
.hexpand(true)
.margin_bottom(MARGIN)
.margin_end(MARGIN)
.margin_start(MARGIN)
.margin_top(MARGIN)
.placeholder_text("Find in text..")
.primary_icon_activatable(false)
.primary_icon_name("system-search-symbolic")
.primary_icon_sensitive(false)
.valign(Align::Center)
.vexpand(false)
.build();
// Connect events
entry.connect_icon_release(|this, position| match position {
EntryIconPosition::Secondary => clean(this),
_ => todo!(), // unexpected
});
entry.connect_changed(|this| {
// toggle entry clear button
if this.text().is_empty() {
this.set_secondary_icon_name(None);
} else {
this.set_secondary_icon_name(Some("edit-clear-symbolic"));
}
});
// Done
Self { entry }
}
// Actions
pub fn clean(&self) {
clean(&self.entry)
}
pub fn update(&self, is_match: bool) {
if is_match {
self.entry.remove_css_class("error");
} else {
self.entry.add_css_class("error");
}
}
}
fn clean(entry: &Entry) {
entry.delete_text(0, -1)
}

View file

@ -0,0 +1,8 @@
use gtk::{gdk::Cursor, CheckButton};
pub fn new() -> CheckButton {
CheckButton::builder()
.cursor(&Cursor::from_name("default", None).unwrap())
.label("Match case")
.build()
}

View file

@ -0,0 +1,137 @@
mod back;
mod forward;
use back::Back;
use forward::Forward;
use gtk::{
prelude::{BoxExt, TextBufferExt},
Box, Orientation, TextBuffer, TextIter, TextTag,
};
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
const MARGIN: i32 = 6;
pub struct Navigation {
pub back: Back,
pub forward: Forward,
pub g_box: Box,
index: Rc<Cell<usize>>,
matches: Rc<RefCell<Vec<(TextIter, TextIter)>>>,
}
impl Navigation {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
// Init shared matches holder
let index = Rc::new(Cell::new(0));
let matches = Rc::new(RefCell::new(Vec::new()));
// Init components
let back = Back::new();
let forward = Forward::new();
// Init main container
let g_box = Box::builder()
.css_classes([
"linked", // merge childs
])
.margin_end(MARGIN)
.orientation(Orientation::Horizontal)
.build();
g_box.append(&back.button);
g_box.append(&forward.button);
Self {
back,
forward,
g_box,
index,
matches,
}
}
// Actions
pub fn update(&self, matches: Vec<(TextIter, TextIter)>) {
// Update self
self.matches.replace(matches);
self.index.replace(0); // reset
// Update child components
self.back.update(self.is_match());
self.forward.update(self.is_match());
}
/*
pub fn back(&self) -> Option<(TextIter, TextIter)> {
self.text_buffer.remove_tag(
&self.current_tag,
&self.text_buffer.start_iter(),
&self.text_buffer.end_iter(),
);
let index = self.index.take();
match self.matches.borrow().get(back(index)) {
Some((start, end)) => {
self.text_buffer.apply_tag(&self.current_tag, start, end);
self.index.replace(if index == 0 {
len_to_index(self.matches.borrow().len())
} else {
index
});
Some((*start, *end))
}
None => {
self.index
.replace(len_to_index(self.matches.borrow().len())); // go last
None
}
}
}
pub fn forward(&self) -> Option<(TextIter, TextIter)> {
self.text_buffer.remove_tag(
&self.current_tag,
&self.text_buffer.start_iter(),
&self.text_buffer.end_iter(),
);
let index = self.index.take();
let next = forward(index);
match self.matches.borrow().get(next) {
Some((start, end)) => {
self.text_buffer.apply_tag(&self.current_tag, start, end);
self.index.replace(next);
Some((*start, *end))
}
None => {
self.index.replace(0);
None
}
}
}
*/
// Getters
pub fn is_match(&self) -> bool {
!self.matches.borrow().is_empty()
}
}
fn back(index: usize) -> usize {
index - 1
}
fn forward(index: usize) -> usize {
index + 1
}
fn len_to_index(len: usize) -> usize {
len - 1
}

View file

@ -0,0 +1,29 @@
use gtk::{gdk::Cursor, prelude::WidgetExt, Align, Button};
pub struct Back {
pub button: Button,
}
impl Back {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
Self {
button: Button::builder()
.cursor(&Cursor::from_name("default", None).unwrap())
.icon_name("go-up-symbolic")
.sensitive(false)
.tooltip_text("Back")
.valign(Align::Center)
.vexpand(false)
.build(),
}
}
// Actions
pub fn update(&self, is_sensitive: bool) {
self.button.set_sensitive(is_sensitive);
}
}

View file

@ -0,0 +1,29 @@
use gtk::{gdk::Cursor, prelude::WidgetExt, Align, Button};
pub struct Forward {
pub button: Button,
}
impl Forward {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
Self {
button: Button::builder()
.cursor(&Cursor::from_name("default", None).unwrap())
.icon_name("go-down-symbolic")
.sensitive(false)
.tooltip_text("Forward")
.valign(Align::Center)
.vexpand(false)
.build(),
}
}
// Actions
pub fn update(&self, is_sensitive: bool) {
self.button.set_sensitive(is_sensitive);
}
}

View file

@ -0,0 +1,29 @@
use gtk::{prelude::WidgetExt, Label};
pub struct Placeholder {
pub label: Label,
}
impl Placeholder {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
Self {
label: Label::builder()
.css_classes(["error"])
.label("Search action requires activation!")
.build(),
}
}
// Actions
pub fn show(&self) {
self.label.set_visible(true)
}
pub fn hide(&self) {
self.label.set_visible(false)
}
}