implement search match highlight

This commit is contained in:
yggverse 2024-12-15 13:56:30 +02:00
parent db44fff212
commit 1b2aeae665
5 changed files with 91 additions and 99 deletions

View file

@ -9,9 +9,8 @@ use syntax::Syntax;
use tag::Tag;
use widget::Widget;
use crate::app::browser::window::{
action::Position, tab::item::Action as TabAction, Action as WindowAction,
};
use super::{TabAction, WindowAction};
use crate::app::browser::window::action::Position;
use gemtext::line::{
code::{Inline, Multiline},
header::{Header, Level},
@ -24,8 +23,8 @@ use gtk::{
gio::Cancellable,
glib::{TimeZone, Uri},
prelude::{TextBufferExt, TextBufferExtManual, TextTagExt, TextViewExt, WidgetExt},
EventControllerMotion, GestureClick, TextBuffer, TextSearchFlags, TextTag, TextWindowType,
UriLauncher, Window, WrapMode,
EventControllerMotion, GestureClick, TextBuffer, TextTag, TextWindowType, UriLauncher, Window,
WrapMode,
};
use std::{cell::Cell, collections::HashMap, rc::Rc};
@ -39,8 +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 {
buffer: TextBuffer,
tag: Tag,
pub title: Option<String>,
pub widget: Rc<Widget>,
}
@ -50,7 +47,7 @@ impl Reader {
pub fn new(
gemtext: &str,
base: &Uri,
actions: (Rc<WindowAction>, Rc<TabAction>),
(window_action, tab_action): (Rc<WindowAction>, Rc<TabAction>),
) -> Result<Self, Error> {
// Init default values
let mut title = None;
@ -319,10 +316,11 @@ impl Reader {
// Init widget
let widget = Rc::new(Widget::new(
&window_action,
&buffer,
primary_button_controller.clone(),
middle_button_controller.clone(),
motion_controller.clone(),
&primary_button_controller,
&middle_button_controller,
&motion_controller,
));
// Init shared reference container for HashTable collected
@ -348,7 +346,7 @@ impl Reader {
return match uri.scheme().as_str() {
"gemini" => {
// Open new page in browser
actions.1.load.activate(Some(&uri.to_str()), true);
tab_action.load.activate(Some(&uri.to_str()), true);
}
// Scheme not supported, delegate
_ => UriLauncher::new(&uri.to_str()).launch(
@ -385,7 +383,7 @@ impl Reader {
return match uri.scheme().as_str() {
"gemini" => {
// Open new page in browser
actions.0.append.activate_stateful_once(
window_action.append.activate_stateful_once(
Position::After,
Some(uri.to_string()),
false,
@ -461,33 +459,7 @@ impl Reader {
}); // @TODO may be expensive for CPU, add timeout?
// Result
Ok(Self {
buffer,
tag,
title,
widget,
})
}
// Actions
pub fn find(&self) {
self.buffer.remove_tag(
&self.tag.found.text_tag,
&self.buffer.start_iter(),
&self.buffer.end_iter(),
);
let mut next = self.buffer.start_iter();
while let Some((start, end)) = next.forward_search(
"Gemini",
TextSearchFlags::CASE_INSENSITIVE, // @TODO
None, // unlimited
) {
self.buffer
.apply_tag(&self.tag.found.text_tag, &start, &end);
next = end;
}
Ok(Self { title, widget })
}
}

View file

@ -1,4 +1,3 @@
mod found;
mod h1;
mod h2;
mod h3;
@ -6,7 +5,6 @@ mod list;
mod quote;
mod title;
use found::Found;
use h1::H1;
use h2::H2;
use h3::H3;
@ -25,7 +23,6 @@ pub struct Tag {
pub list: List,
pub quote: Quote,
pub title: Title,
pub found: Found,
}
impl Tag {
@ -38,7 +35,6 @@ impl Tag {
let list = List::new();
let quote = Quote::new();
let title = Title::new();
let found = Found::new();
// Init tag table
let text_tag_table = TextTagTable::new();
@ -49,7 +45,6 @@ impl Tag {
text_tag_table.add(&title.text_tag);
text_tag_table.add(&list.text_tag);
text_tag_table.add(&quote.text_tag);
text_tag_table.add(&found.text_tag);
Self {
text_tag_table,
@ -60,7 +55,6 @@ impl Tag {
list,
quote,
title,
found,
}
}
}

View file

@ -1,19 +0,0 @@
use gtk::{gdk::RGBA, TextTag, WrapMode};
pub struct Found {
pub text_tag: TextTag,
}
impl Found {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
Self {
text_tag: TextTag::builder()
.background_rgba(&RGBA::new(0.502, 0.502, 0.502, 0.5)) // @TODO
.wrap_mode(WrapMode::Word)
.build(),
}
}
}

View file

@ -1,16 +1,17 @@
mod find;
use std::rc::Rc;
use find::Find;
use super::WindowAction;
use gtk::{
prelude::{TextViewExt, WidgetExt},
prelude::{ButtonExt, TextViewExt, WidgetExt},
EventControllerMotion, GestureClick, TextBuffer, TextView, TextWindowType, WrapMode,
};
const MARGIN: i32 = 8;
pub struct Widget {
find: Find,
pub text_view: TextView,
}
@ -19,13 +20,14 @@ impl Widget {
/// Create new `Self`
pub fn new(
action: &WindowAction,
buffer: &TextBuffer,
primary_button_controller: GestureClick,
middle_button_controller: GestureClick,
motion_controller: EventControllerMotion,
primary_button_controller: &GestureClick,
middle_button_controller: &GestureClick,
motion_controller: &EventControllerMotion,
) -> Self {
// Init components
let find = Find::new();
let find = Rc::new(Find::new(buffer));
// Init main widget
let text_view = TextView::builder()
@ -40,24 +42,26 @@ impl Widget {
.wrap_mode(WrapMode::Word)
.build();
text_view.add_controller(primary_button_controller);
text_view.add_controller(middle_button_controller);
text_view.add_controller(motion_controller);
text_view.add_controller(primary_button_controller.clone());
text_view.add_controller(middle_button_controller.clone());
text_view.add_controller(motion_controller.clone());
// Connect events
action.find.connect_activate({
let find = find.clone();
let text_view = text_view.clone();
move |_| {
text_view.set_gutter(TextWindowType::Bottom, Some(&find.g_box));
find.entry.grab_focus();
}
});
find.close.connect_clicked({
let text_view = text_view.clone();
move |_| text_view.set_gutter(TextWindowType::Bottom, gtk::Widget::NONE)
});
// Done
Self { find, text_view }
}
// Actions
pub fn find(&self, is_visible: bool) {
if is_visible {
self.text_view
.set_gutter(TextWindowType::Bottom, Some(&self.find.g_box));
self.find.g_box.grab_focus();
} else {
self.text_view
.set_gutter(TextWindowType::Bottom, gtk::Widget::NONE);
}
Self { text_view }
}
}

View file

@ -1,18 +1,20 @@
use gtk::{
gdk::Cursor,
prelude::{BoxExt, EditableExt, EntryExt},
Box, Button, Entry, EntryIconPosition, Orientation,
gdk::{Cursor, RGBA},
prelude::{BoxExt, ButtonExt, EditableExt, EntryExt, TextBufferExt},
Box, Button, Entry, EntryIconPosition, Orientation, TextBuffer, TextSearchFlags, TextTag,
};
const MARGIN: i32 = 6;
pub struct Find {
pub close: Button,
pub entry: Entry,
pub g_box: Box,
}
impl Find {
// Construct
pub fn new() -> Self {
pub fn new(text_buffer: &TextBuffer) -> Self {
// Init components
let close = Button::builder()
.cursor(&Cursor::from_name("default", None).unwrap())
@ -34,6 +36,11 @@ impl Find {
.primary_icon_name("system-search-symbolic")
.build();
let text_tag = TextTag::builder()
.background_rgba(&RGBA::new(0.502, 0.502, 0.502, 0.5)) // @TODO
.build();
text_buffer.tag_table().add(&text_tag);
// Init main container
let g_box = Box::builder().orientation(Orientation::Horizontal).build();
@ -41,13 +48,43 @@ impl Find {
g_box.append(&close);
// Connect events
entry.connect_activate(|_| {}); // @TODO
close.connect_clicked({
let entry = entry.clone();
move |_| entry.delete_text(0, -1)
});
entry.connect_changed(move |this| {
if this.text().is_empty() {
this.set_secondary_icon_name(None);
} else {
this.set_secondary_icon_name(Some("edit-clear-symbolic"));
entry.connect_changed({
let entry = entry.clone();
let text_buffer = text_buffer.clone();
let text_tag = text_tag.clone();
move |this| {
// Toggle clear action
if this.text().is_empty() {
this.set_secondary_icon_name(None);
} else {
this.set_secondary_icon_name(Some("edit-clear-symbolic"));
}
// Cleanup previous search results
text_buffer.remove_tag(
&text_tag,
&text_buffer.start_iter(),
&text_buffer.end_iter(),
);
// Get subject once
let query = entry.text();
// Begin search
let mut next = text_buffer.start_iter();
while let Some((start, end)) = next.forward_search(
&query,
TextSearchFlags::CASE_INSENSITIVE, // @TODO
None, // unlimited
) {
text_buffer.apply_tag(&text_tag, &start, &end);
next = end;
}
}
});
@ -57,6 +94,10 @@ impl Find {
});
// Done
Self { g_box }
Self {
close,
entry,
g_box,
}
}
}