mirror of
https://github.com/YGGverse/Yoda.git
synced 2026-04-01 17:15:28 +00:00
init search on page feature once
This commit is contained in:
parent
4b357f8229
commit
f767c11789
23 changed files with 449 additions and 266 deletions
|
|
@ -1,17 +1,14 @@
|
|||
mod gemini;
|
||||
mod search;
|
||||
mod source;
|
||||
|
||||
use gemini::Gemini;
|
||||
use search::Search;
|
||||
use source::Source;
|
||||
|
||||
use super::{BrowserAction, TabAction, WindowAction};
|
||||
use adw::Clamp;
|
||||
use gtk::{
|
||||
glib::Uri,
|
||||
prelude::{BoxExt, ButtonExt, TextViewExt, WidgetExt},
|
||||
Box, Orientation, ScrolledWindow,
|
||||
prelude::{BoxExt, TextViewExt},
|
||||
Box, Orientation, ScrolledWindow, TextBuffer,
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
|
|
@ -20,8 +17,8 @@ pub struct Meta {
|
|||
} // @TODO move to separated mod
|
||||
|
||||
pub struct Text {
|
||||
pub buffer: TextBuffer,
|
||||
pub g_box: Box,
|
||||
pub has_search: bool,
|
||||
pub meta: Meta,
|
||||
}
|
||||
|
||||
|
|
@ -39,7 +36,6 @@ impl Text {
|
|||
) -> Self {
|
||||
// Init components
|
||||
let gemini = Gemini::new(gemtext, base, (window_action, tab_action));
|
||||
let search = Rc::new(Search::new(&gemini.reader.buffer));
|
||||
|
||||
// Init main widget
|
||||
let g_box = Box::builder().orientation(Orientation::Vertical).build();
|
||||
|
|
@ -50,15 +46,8 @@ impl Text {
|
|||
.build(),
|
||||
);
|
||||
|
||||
g_box.append(
|
||||
&Clamp::builder()
|
||||
.child(&search.g_box)
|
||||
.css_classes(["osd"])
|
||||
.maximum_size(800)
|
||||
.build(),
|
||||
);
|
||||
|
||||
// Connect events
|
||||
/* @TODO
|
||||
browser_action.escape.connect_activate({
|
||||
let close = search.close.clone();
|
||||
move || {
|
||||
|
|
@ -99,29 +88,28 @@ impl Text {
|
|||
move |_| {
|
||||
search.g_box.set_visible(false);
|
||||
}
|
||||
});
|
||||
});*/
|
||||
|
||||
Self {
|
||||
buffer: gemini.reader.widget.text_view.buffer(),
|
||||
meta: Meta {
|
||||
title: gemini.reader.title.clone(),
|
||||
},
|
||||
has_search: true,
|
||||
g_box,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_source(data: &str) -> Self {
|
||||
// Init components
|
||||
let source = Source::new(data);
|
||||
|
||||
let g_box = Box::builder().orientation(Orientation::Vertical).build();
|
||||
|
||||
g_box.append(
|
||||
&ScrolledWindow::builder()
|
||||
.child(&Source::new(data).text_view)
|
||||
.build(),
|
||||
);
|
||||
g_box.append(&ScrolledWindow::builder().child(&source.text_view).build());
|
||||
|
||||
Self {
|
||||
buffer: source.text_view.buffer(),
|
||||
meta: Meta { title: None },
|
||||
has_search: false,
|
||||
g_box,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ const LINK_COLOR_DEFAULT: (f32, f32, f32, f32) = (53.0, 132.0, 228.0, 255.0);
|
|||
const LINK_COLOR_ONHOVER: (f32, f32, f32, f32) = (53.0, 132.0, 228.0, 228.0);
|
||||
|
||||
pub struct Reader {
|
||||
pub buffer: TextBuffer,
|
||||
pub title: Option<String>,
|
||||
pub widget: Rc<Widget>,
|
||||
}
|
||||
|
|
@ -459,11 +458,7 @@ impl Reader {
|
|||
}); // @TODO may be expensive for CPU, add timeout?
|
||||
|
||||
// Result
|
||||
Ok(Self {
|
||||
buffer,
|
||||
title,
|
||||
widget,
|
||||
})
|
||||
Ok(Self { title, widget })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,130 +0,0 @@
|
|||
mod close;
|
||||
mod input;
|
||||
mod match_case;
|
||||
mod navigation;
|
||||
mod tag;
|
||||
|
||||
use input::Input;
|
||||
use navigation::Navigation;
|
||||
use tag::Tag;
|
||||
|
||||
use gtk::{
|
||||
prelude::{BoxExt, ButtonExt, CheckButtonExt, EditableExt, TextBufferExt},
|
||||
Align, Box, Button, Orientation, TextBuffer, TextIter, TextSearchFlags,
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct Search {
|
||||
pub close: Button,
|
||||
pub g_box: Box,
|
||||
pub input: Rc<Input>,
|
||||
pub navigation: Rc<Navigation>,
|
||||
}
|
||||
|
||||
impl Search {
|
||||
// Construct
|
||||
pub fn new(buffer: &TextBuffer) -> Self {
|
||||
// Init components
|
||||
let close = close::new();
|
||||
let input = Rc::new(Input::new());
|
||||
let match_case = match_case::new();
|
||||
let tag = Rc::new(Tag::new(buffer.tag_table()));
|
||||
let navigation = Rc::new(Navigation::new(buffer.clone(), tag.current.clone()));
|
||||
|
||||
// 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 tag = tag.clone();
|
||||
let buffer = buffer.clone();
|
||||
move |_| {
|
||||
navigation.update(find(
|
||||
&buffer,
|
||||
&tag,
|
||||
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 tag = tag.clone();
|
||||
let buffer = buffer.clone();
|
||||
move |this| {
|
||||
navigation.update(find(
|
||||
&buffer,
|
||||
&tag,
|
||||
input.entry.text().as_str(),
|
||||
this.is_active(),
|
||||
));
|
||||
input.update(navigation.is_match());
|
||||
}
|
||||
});
|
||||
|
||||
// Done
|
||||
Self {
|
||||
close,
|
||||
g_box,
|
||||
input,
|
||||
navigation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tools
|
||||
|
||||
fn find(
|
||||
buffer: &TextBuffer,
|
||||
tag: &Rc<Tag>,
|
||||
subject: &str,
|
||||
is_match_case: bool,
|
||||
) -> Vec<(TextIter, TextIter)> {
|
||||
// Init matches holder
|
||||
let mut result = Vec::new();
|
||||
|
||||
// Get iters
|
||||
let buffer_start = buffer.start_iter();
|
||||
let buffer_end = buffer.end_iter();
|
||||
|
||||
// Cleanup previous search highlights
|
||||
buffer.remove_tag(&tag.current, &buffer_start, &buffer_end);
|
||||
buffer.remove_tag(&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.apply_tag(&tag.found, &match_start, &match_end);
|
||||
next = match_end;
|
||||
result.push((match_start, match_end));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
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()
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
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)
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
use gtk::{gdk::Cursor, CheckButton};
|
||||
|
||||
pub fn new() -> CheckButton {
|
||||
CheckButton::builder()
|
||||
.cursor(&Cursor::from_name("default", None).unwrap())
|
||||
.label("Match case")
|
||||
.build()
|
||||
}
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
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)>>>,
|
||||
text_buffer: TextBuffer,
|
||||
current_tag: TextTag,
|
||||
}
|
||||
|
||||
impl Navigation {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
pub fn new(text_buffer: TextBuffer, current_tag: TextTag) -> 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,
|
||||
text_buffer,
|
||||
current_tag,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
mod current;
|
||||
mod found;
|
||||
|
||||
use gtk::{TextTag, TextTagTable};
|
||||
|
||||
pub struct Tag {
|
||||
pub current: TextTag,
|
||||
pub found: TextTag,
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
// Constructors
|
||||
|
||||
pub fn new(tag_table: TextTagTable) -> Self {
|
||||
// Init components
|
||||
let current = current::new();
|
||||
let found = found::new();
|
||||
|
||||
// Init `Self`
|
||||
tag_table.add(&found);
|
||||
tag_table.add(¤t); // keep current priority as `current` should overwrite `found`
|
||||
// https://docs.gtk.org/gtk4/method.TextTag.set_priority.html
|
||||
|
||||
Self { current, found }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
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()
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
use gtk::{gdk::RGBA, TextTag};
|
||||
|
||||
pub fn new() -> TextTag {
|
||||
TextTag::builder()
|
||||
.background_rgba(&RGBA::new(0.502, 0.502, 0.502, 0.5)) // @TODO
|
||||
.build()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue