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

@ -71,7 +71,9 @@ impl Browser {
action.escape.connect_activate({
let widget = widget.clone();
let window = window.clone();
move || {
window.tab.escape(None); // current tab
widget.application_window.set_focus(gtk::Window::NONE);
}
});

View File

@ -81,6 +81,11 @@ impl Window {
move |_| tab.close_all()
});
action.find.connect_activate({
let tab = tab.clone();
move |position| tab.find(position)
});
action.save_as.connect_activate({
let tab = tab.clone();
move |position| tab.save_as(position)

View File

@ -166,9 +166,9 @@ impl Tab {
item
}
/// Close page at given `position`, `None` to close selected page (if available)
pub fn close(&self, position: Option<i32>) {
self.widget.close(position);
/// Close page at given `page_position`, `None` to close selected page (if available)
pub fn close(&self, page_position: Option<i32>) {
self.widget.close(page_position);
}
// Close all pages
@ -176,6 +176,20 @@ impl Tab {
self.widget.close_all();
}
// Toggle search widget
pub fn escape(&self, page_position: Option<i32>) {
if let Some(item) = self.item(page_position) {
item.page.escape();
}
}
// Toggle search widget
pub fn find(&self, page_position: Option<i32>) {
if let Some(item) = self.item(page_position) {
item.page.find();
}
}
// Save page at given `position`, `None` to save selected page (if available)
pub fn save_as(&self, page_position: Option<i32>) {
if let Some(item) = self.item(page_position) {

View File

@ -6,6 +6,7 @@ mod input;
mod meta;
mod navigation;
mod request;
mod search;
mod widget;
use client::Client;
@ -15,6 +16,7 @@ use input::Input;
use meta::{Meta, Status};
use navigation::Navigation;
use request::Request;
use search::Search;
use widget::Widget;
use crate::app::browser::{
@ -42,6 +44,7 @@ pub struct Page {
// Components
pub client: Rc<Client>,
pub content: Rc<Content>,
pub search: Rc<Search>,
pub input: Rc<Input>,
pub meta: Rc<Meta>,
pub navigation: Rc<Navigation>,
@ -67,6 +70,8 @@ impl Page {
tab_action.clone(),
)));
let search = Rc::new(Search::new());
let navigation = Rc::new(Navigation::new(
profile.clone(),
(
@ -82,6 +87,7 @@ impl Page {
&id,
&navigation.widget.g_box,
&content.g_box,
&search.g_box,
&input.widget.clamp,
));
@ -98,9 +104,10 @@ impl Page {
// Components
client: Rc::new(Client::new()),
content,
navigation,
search,
input,
meta,
navigation,
widget,
}
}
@ -122,6 +129,16 @@ impl Page {
result
}
/// Request `Escape` action for child components
pub fn escape(&self) {
self.search.hide()
}
/// Toggle `Find` widget
pub fn find(&self) {
self.search.toggle()
}
/// Navigate home URL (parsed from current navigation entry)
/// * this method create new history record in memory as defined in `action_page_open` action
pub fn home(&self) {
@ -169,6 +186,7 @@ impl Page {
self.window_action.find.simple_action.set_enabled(false);
// Reset widgets
self.search.update(None);
self.input.unset();
// Prevent infinitive redirection
@ -372,21 +390,22 @@ impl Page {
use gemini::client::connection::response;
// Init shared clones
let browser_action = self.browser_action.clone();
let cancellable = self.client.cancellable();
let content = self.content.clone();
let find = self.window_action.find.clone();
let search = self.search.clone();
let id = self.id.clone();
let input = self.input.clone();
let meta = self.meta.clone();
let navigation = self.navigation.clone();
let tab_action = self.tab_action.clone();
let update = self.browser_action.update.clone();
let window_action = self.window_action.clone();
// Listen for connection status updates
self.client.gemini.socket.connect_event({
let id = id.clone();
let meta = meta.clone();
let update = update.clone();
let update = browser_action.update.clone();
move |_, event, _, _| {
meta.set_status(match event {
SocketClientEvent::Resolving => Status::Resolving,
@ -455,7 +474,7 @@ impl Page {
.set_title(&title);
// Update page
update.activate(Some(&id));
browser_action.update.activate(Some(&id));
},
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-20
response::meta::Status::Success => {
@ -522,7 +541,7 @@ impl Page {
.set_title(&status.title());
// Update window
update.activate(Some(&id));
browser_action.update.activate(Some(&id));
} else { // browse
match response.meta.mime.unwrap().value.to_lowercase().as_str() {
"text/gemini" => {
@ -532,18 +551,19 @@ impl Page {
Priority::DEFAULT,
cancellable.clone(),
{
let browser_action = browser_action.clone();
let content = content.clone();
let find = find.clone();
let search = search.clone();
let id = id.clone();
let meta = meta.clone();
let update = update.clone();
let uri = uri.clone();
let window_action = window_action.clone();
move |result|{
match result {
Ok(buffer) => {
// Set children component,
// extract title from meta parsed
let text = if is_source {
let text_widget = if is_source {
content.to_text_source(
&buffer.data
)
@ -554,16 +574,20 @@ impl Page {
)
};
// Update `find` model with new buffer
search.update(Some(text_widget.buffer));
// Update page meta
meta.set_status(Status::Success)
.set_title(&match text.meta.title {
.set_title(&match text_widget.meta.title {
Some(meta_title) => meta_title,
None => uri_to_title(&uri)
});
// Update window components
find.simple_action.set_enabled(text.has_search);
update.activate(Some(&id));
window_action.find.simple_action.set_enabled(true);
browser_action.update.activate(Some(&id));
}
Err(e) => {
// Update widget
@ -575,7 +599,7 @@ impl Page {
.set_title(&status.title());
// Update window
update.activate(Some(&id));
browser_action.update.activate(Some(&id));
},
}
}
@ -603,11 +627,11 @@ impl Page {
);
},
{
let browser_action = browser_action.clone();
let cancellable = cancellable.clone();
let content = content.clone();
let id = id.clone();
let meta = meta.clone();
let update = update.clone();
let uri = uri.clone();
move |result| match result {
Ok((memory_input_stream, _)) => {
@ -626,7 +650,7 @@ impl Page {
content.to_image(&Texture::for_pixbuf(&buffer));
// Update window components
update.activate(Some(&id));
browser_action.update.activate(Some(&id));
}
Err(e) => {
// Update widget
@ -666,7 +690,7 @@ impl Page {
.set_title(&status.title());
// Update window
update.activate(Some(&id));
browser_action.update.activate(Some(&id));
},
}
}
@ -773,7 +797,7 @@ impl Page {
},
}
update.activate(Some(&id));
browser_action.update.activate(Some(&id));
},
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60
response::meta::Status::CertificateRequest |
@ -803,7 +827,7 @@ impl Page {
.set_title(&status.title());
// Update window
update.activate(Some(&id));
browser_action.update.activate(Some(&id));
}
_ => {
// Add history record
@ -824,7 +848,7 @@ impl Page {
.set_title(&status.title());
// Update window
update.activate(Some(&id));
browser_action.update.activate(Some(&id));
}
}
},
@ -843,7 +867,7 @@ impl Page {
.set_title(&status.title());
// Update window
update.activate(Some(&id));
browser_action.update.activate(Some(&id));
}
}
);

View File

@ -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,
}
}

View File

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

View File

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

View File

@ -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(&current); // keep current priority as `current` should overwrite `found`
// https://docs.gtk.org/gtk4/method.TextTag.set_priority.html
Self { current, found }
}
}

View File

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

View File

@ -0,0 +1,83 @@
mod buffer;
mod form;
mod placeholder;
use buffer::Buffer;
use form::Form;
use placeholder::Placeholder;
use gtk::{
prelude::{BoxExt, WidgetExt},
Align, Box, Orientation, TextBuffer,
};
use std::{cell::RefCell, rc::Rc};
pub struct Search {
buffer: Rc<RefCell<Option<Buffer>>>,
pub form: Rc<Form>,
pub placeholder: Rc<Placeholder>,
pub g_box: Box,
}
impl Search {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
// Init components
let buffer = Rc::new(RefCell::new(None));
let form = Rc::new(Form::new(&buffer));
let placeholder = Rc::new(Placeholder::new());
// Init main container
let g_box = Box::builder()
.orientation(Orientation::Vertical)
.valign(Align::Center)
.vexpand(false)
.visible(false)
.build();
g_box.append(&form.g_box);
g_box.append(&placeholder.label);
// Done
Self {
buffer,
form,
g_box,
placeholder,
}
}
// Actions
pub fn show(&self) {
if self.buffer.borrow().is_some() {
self.form.show();
self.placeholder.hide();
} else {
self.form.hide();
self.placeholder.show();
}
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()
}
}
pub fn update(&self, text_buffer: Option<TextBuffer>) {
self.buffer.replace(match text_buffer {
Some(buffer) => Some(Buffer::new(buffer)),
None => None,
});
}
}

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,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

@ -21,15 +21,13 @@ pub struct Navigation {
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 {
pub fn new() -> Self {
// Init shared matches holder
let index = Rc::new(Cell::new(0));
let matches = Rc::new(RefCell::new(Vec::new()));
@ -56,8 +54,6 @@ impl Navigation {
g_box,
index,
matches,
text_buffer,
current_tag,
}
}
@ -72,55 +68,55 @@ impl Navigation {
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(),
);
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
}
}
}
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
}
}
}
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 {

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

View File

@ -14,6 +14,7 @@ impl Widget {
// Components
navigation: &impl IsA<gtk::Widget>,
content: &impl IsA<gtk::Widget>,
search: &impl IsA<gtk::Widget>,
input: &impl IsA<gtk::Widget>,
) -> Self {
// Init self
@ -24,6 +25,7 @@ impl Widget {
g_box.append(navigation);
g_box.append(content);
g_box.append(search);
g_box.append(input);
Self { g_box }