diff --git a/src/app/browser/window/tab/item/client/driver/file/text.rs b/src/app/browser/window/tab/item/client/driver/file/text.rs index 36dba3dc..5a0d99f6 100644 --- a/src/app/browser/window/tab/item/client/driver/file/text.rs +++ b/src/app/browser/window/tab/item/client/driver/file/text.rs @@ -21,7 +21,7 @@ impl Text { .info .borrow_mut() .set_mime(Some("text/gemini".to_string())); - page.content.to_text_gemini(uri, data) + page.content.to_text_gemini(&page.profile, uri, data) }), Self::Markdown(uri, data) => (uri, { page.navigation @@ -29,7 +29,7 @@ impl Text { .info .borrow_mut() .set_mime(Some("text/markdown".to_string())); - page.content.to_text_markdown(uri, data) + page.content.to_text_markdown(&page.profile, uri, data) }), Self::Plain(uri, data) => (uri, page.content.to_text_plain(data)), Self::Source(uri, data) => (uri, page.content.to_text_source(data)), diff --git a/src/app/browser/window/tab/item/client/driver/gemini.rs b/src/app/browser/window/tab/item/client/driver/gemini.rs index 1c34dc73..3830c605 100644 --- a/src/app/browser/window/tab/item/client/driver/gemini.rs +++ b/src/app/browser/window/tab/item/client/driver/gemini.rs @@ -357,8 +357,8 @@ fn handle( page.content.to_text_source(data) } else { match m.as_str() { - "text/gemini" => page.content.to_text_gemini(&uri, data), - "text/markdown" => page.content.to_text_markdown(&uri, data), + "text/gemini" => page.content.to_text_gemini(&page.profile, &uri, data), + "text/markdown" => page.content.to_text_markdown(&page.profile, &uri, data), "text/plain" => page.content.to_text_plain(data), _ => panic!() // unexpected } diff --git a/src/app/browser/window/tab/item/client/driver/nex.rs b/src/app/browser/window/tab/item/client/driver/nex.rs index da3b2231..919c8869 100644 --- a/src/app/browser/window/tab/item/client/driver/nex.rs +++ b/src/app/browser/window/tab/item/client/driver/nex.rs @@ -299,7 +299,7 @@ fn render( } else if q.ends_with("/") { p.content.to_text_nex(&u, d) } else if q.ends_with(".gmi") || q.ends_with(".gemini") { - p.content.to_text_gemini(&u, d) + p.content.to_text_gemini(&p.profile, &u, d) } else { p.content.to_text_plain(d) }; diff --git a/src/app/browser/window/tab/item/page/content.rs b/src/app/browser/window/tab/item/page/content.rs index fc88758d..aadd1b88 100644 --- a/src/app/browser/window/tab/item/page/content.rs +++ b/src/app/browser/window/tab/item/page/content.rs @@ -7,6 +7,8 @@ use directory::Directory; use image::Image; use text::Text; +use crate::profile::Profile; + use super::{ItemAction, TabAction, WindowAction}; use adw::StatusPage; use gtk::{ @@ -126,9 +128,14 @@ impl Content { } /// `text/gemini` - pub fn to_text_gemini(&self, base: &Uri, data: &str) -> Text { + pub fn to_text_gemini(&self, profile: &Rc, base: &Uri, data: &str) -> Text { self.clean(); - match Text::gemini((&self.window_action, &self.item_action), base, data) { + match Text::gemini( + (&self.window_action, &self.item_action), + profile, + base, + data, + ) { Ok(text) => { self.g_box.append(&text.scrolled_window); text @@ -155,9 +162,14 @@ impl Content { } /// `text/markdown` - pub fn to_text_markdown(&self, base: &Uri, data: &str) -> Text { + pub fn to_text_markdown(&self, profile: &Rc, base: &Uri, data: &str) -> Text { self.clean(); - let m = Text::markdown((&self.window_action, &self.item_action), base, data); + let m = Text::markdown( + (&self.window_action, &self.item_action), + profile, + base, + data, + ); self.g_box.append(&m.scrolled_window); m } diff --git a/src/app/browser/window/tab/item/page/content/text.rs b/src/app/browser/window/tab/item/page/content/text.rs index 9a634185..d5f91393 100644 --- a/src/app/browser/window/tab/item/page/content/text.rs +++ b/src/app/browser/window/tab/item/page/content/text.rs @@ -4,6 +4,8 @@ mod nex; mod plain; mod source; +use crate::profile::Profile; + use super::{ItemAction, WindowAction}; use adw::ClampScrollable; use gemini::Gemini; @@ -27,10 +29,11 @@ pub struct Text { impl Text { pub fn gemini( actions: (&Rc, &Rc), + profile: &Rc, base: &Uri, gemtext: &str, ) -> Result)> { - match Gemini::build(actions, base, gemtext) { + match Gemini::build(actions, profile, base, gemtext) { Ok(widget) => Ok(Self { scrolled_window: reader(&widget.text_view), text_view: widget.text_view, @@ -55,10 +58,11 @@ impl Text { pub fn markdown( actions: (&Rc, &Rc), + profile: &Rc, base: &Uri, gemtext: &str, ) -> Self { - let markdown = Markdown::build(actions, base, gemtext); + let markdown = Markdown::build(actions, profile, base, gemtext); Self { scrolled_window: reader(&markdown.text_view), text_view: markdown.text_view, diff --git a/src/app/browser/window/tab/item/page/content/text/gemini.rs b/src/app/browser/window/tab/item/page/content/text/gemini.rs index 8eae79b9..3cf654cb 100644 --- a/src/app/browser/window/tab/item/page/content/text/gemini.rs +++ b/src/app/browser/window/tab/item/page/content/text/gemini.rs @@ -6,13 +6,13 @@ mod syntax; mod tag; use super::{ItemAction, WindowAction}; -use crate::app::browser::window::action::Position; +use crate::{app::browser::window::action::Position, profile::Profile}; pub use error::Error; use gtk::{ EventControllerMotion, GestureClick, TextBuffer, TextTag, TextView, TextWindowType, UriLauncher, Window, WrapMode, - gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, BUTTON_SECONDARY, RGBA}, - gio::{Cancellable, SimpleAction, SimpleActionGroup}, + gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, BUTTON_SECONDARY, Display, RGBA}, + gio::{Cancellable, Menu, SimpleAction, SimpleActionGroup}, glib::{Uri, uuid_string_random}, prelude::{PopoverExt, TextBufferExt, TextBufferExtManual, TextTagExt, TextViewExt, WidgetExt}, }; @@ -36,6 +36,7 @@ impl Gemini { /// Build new `Self` pub fn build( (window_action, item_action): (&Rc, &Rc), + profile: &Rc, base: &Uri, gemtext: &str, ) -> Result { @@ -220,7 +221,7 @@ impl Gemini { let mut alt = Vec::with_capacity(2); if uri.scheme() != base.scheme() { - alt.push("⇖".to_string()); + alt.push(LINK_EXTERNAL_INDICATOR.to_string()); } alt.push(match link.alt { @@ -235,9 +236,7 @@ impl Gemini { .wrap_mode(WrapMode::Word) .build(); - if !tag.text_tag_table.add(&a) { - panic!() - } + assert!(tag.text_tag_table.add(&a)); buffer.insert_with_tags(&mut buffer.end_iter(), &alt.join(" "), &[&a]); buffer.insert(&mut buffer.end_iter(), NEW_LINE); @@ -296,14 +295,39 @@ impl Gemini { ) } }); - let action_link_copy = + let action_link_copy_url = SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_copy.connect_activate(|this, _| { - gtk::gdk::Display::default() + action_link_copy_url.connect_activate(|this, _| { + Display::default() .unwrap() .clipboard() .set_text(&this.state().unwrap().get::().unwrap()) }); + let action_link_copy_text = + SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); + action_link_copy_text.connect_activate(|this, _| { + Display::default() + .unwrap() + .clipboard() + .set_text(&this.state().unwrap().get::().unwrap()) + }); + let action_link_copy_text_selected = + SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); + action_link_copy_text_selected.connect_activate(|this, _| { + Display::default() + .unwrap() + .clipboard() + .set_text(&this.state().unwrap().get::().unwrap()) + }); + let action_link_bookmark = + SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); + action_link_bookmark.connect_activate({ + let p = profile.clone(); + move |this, _| { + let state = this.state().unwrap().get::().unwrap(); + p.bookmark.toggle(&state, None).unwrap(); + } + }); let action_link_download = SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); action_link_download.connect_activate({ @@ -338,14 +362,17 @@ impl Gemini { Some(&{ let g = SimpleActionGroup::new(); g.add_action(&action_link_tab); - g.add_action(&action_link_copy); + g.add_action(&action_link_copy_url); + g.add_action(&action_link_copy_text); + g.add_action(&action_link_copy_text_selected); + g.add_action(&action_link_bookmark); g.add_action(&action_link_download); g.add_action(&action_link_source); g }), ); let link_context = gtk::PopoverMenu::from_model(Some(&{ - let m = gtk::gio::Menu::new(); + let m = Menu::new(); m.append( Some("Open Link in New Tab"), Some(&format!( @@ -353,27 +380,56 @@ impl Gemini { action_link_tab.name() )), ); - m.append( - Some("Copy Link"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_copy.name() - )), - ); - m.append( - Some("Download Link"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_download.name() - )), - ); - m.append( - Some("View Link as Source"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_source.name() - )), - ); + m.append_section(None, &{ + let m_copy = Menu::new(); + m_copy.append( + Some("Copy Link URL"), + Some(&format!( + "{link_context_group_id}.{}", + action_link_copy_url.name() + )), + ); + m_copy.append( + Some("Copy Link Text"), + Some(&format!( + "{link_context_group_id}.{}", + action_link_copy_text.name() + )), + ); + m_copy.append( + Some("Copy Link Text Selected"), + Some(&format!( + "{link_context_group_id}.{}", + action_link_copy_text_selected.name() + )), + ); + m_copy + }); + m.append_section(None, &{ + let m_other = Menu::new(); + m_other.append( + Some("Bookmark Link"), // @TODO highlight state + Some(&format!( + "{link_context_group_id}.{}", + action_link_bookmark.name() + )), + ); + m_other.append( + Some("Download Link"), + Some(&format!( + "{link_context_group_id}.{}", + action_link_download.name() + )), + ); + m_other.append( + Some("View Link as Source"), + Some(&format!( + "{link_context_group_id}.{}", + action_link_source.name() + )), + ); + m_other + }); m })); link_context.set_parent(&text_view); @@ -435,18 +491,61 @@ impl Gemini { let request_str = uri.to_str(); let request_var = request_str.to_variant(); + // Open in the new tab action_link_tab.set_state(&request_var); - action_link_copy.set_state(&request_var); + action_link_copy_text.set_enabled(!request_str.is_empty()); + action_link_copy_url.set_state(&request_var); + action_link_copy_text.set_enabled(!request_str.is_empty()); + + { + // Copy link text + let mut start_iter = iter; + let mut end_iter = iter; + if !start_iter.starts_tag(Some(&tag)) { + start_iter.backward_to_tag_toggle(Some(&tag)); + } + if !end_iter.ends_tag(Some(&tag)) { + end_iter.forward_to_tag_toggle(Some(&tag)); + } + let tagged_text = text_view + .buffer() + .text(&start_iter, &end_iter, false) + .replace(LINK_EXTERNAL_INDICATOR, "") + .trim() + .to_string(); + + action_link_copy_text.set_state(&tagged_text.to_variant()); + action_link_copy_text.set_enabled(!tagged_text.is_empty()); + } + + // Copy link text (if) selected + if let Some((sel_start, sel_end)) = buffer.selection_bounds() { + let selected_tag_text = buffer.text(&sel_start, &sel_end, false); + action_link_copy_text_selected + .set_state(&selected_tag_text.to_variant()); + action_link_copy_text_selected + .set_enabled(!selected_tag_text.is_empty()); + } else { + action_link_copy_text_selected.set_enabled(false); + } + + // Bookmark + action_link_bookmark.set_state(&request_var); + action_link_bookmark.set_enabled(is_prefixable_link(&request_str)); + + // Download (new tab) action_link_download.set_state(&request_var); action_link_download.set_enabled(is_prefixable_link(&request_str)); + // View as Source (new tab) action_link_source.set_state(&request_var); action_link_source.set_enabled(is_prefixable_link(&request_str)); + // Toggle link_context .set_pointing_to(Some(>k::gdk::Rectangle::new(x, y, 1, 1))); - link_context.popup(); + link_context.popup() } } } @@ -580,5 +679,6 @@ fn link_prefix(request: String, prefix: &str) -> String { format!("{prefix}{}", request.trim_start_matches(prefix)) } +const LINK_EXTERNAL_INDICATOR: &str = "⇖"; const LINK_PREFIX_DOWNLOAD: &str = "download:"; const LINK_PREFIX_SOURCE: &str = "source:"; diff --git a/src/app/browser/window/tab/item/page/content/text/markdown.rs b/src/app/browser/window/tab/item/page/content/text/markdown.rs index fb1c8c6c..cf4e8871 100644 --- a/src/app/browser/window/tab/item/page/content/text/markdown.rs +++ b/src/app/browser/window/tab/item/page/content/text/markdown.rs @@ -2,12 +2,12 @@ mod gutter; mod tags; use super::{ItemAction, WindowAction}; -use crate::app::browser::window::action::Position; +use crate::{app::browser::window::action::Position, profile::Profile}; use gtk::{ EventControllerMotion, GestureClick, TextBuffer, TextSearchFlags, TextTag, TextTagTable, TextView, TextWindowType, UriLauncher, Window, WrapMode, - gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, BUTTON_SECONDARY, RGBA}, - gio::{Cancellable, SimpleAction, SimpleActionGroup}, + gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, BUTTON_SECONDARY, Display, RGBA}, + gio::{Cancellable, Menu, SimpleAction, SimpleActionGroup}, glib::{ControlFlow, GString, Uri, idle_add_local, uri_unescape_string, uuid_string_random}, prelude::{PopoverExt, TextBufferExt, TextTagExt, TextViewExt, WidgetExt}, }; @@ -27,6 +27,7 @@ impl Markdown { /// Build new `Self` pub fn build( (window_action, item_action): (&Rc, &Rc), + profile: &Rc, base: &Uri, markdown: &str, ) -> Self { @@ -88,14 +89,39 @@ impl Markdown { ) } }); - let action_link_copy = + let action_link_copy_url = SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_copy.connect_activate(|this, _| { - gtk::gdk::Display::default() + action_link_copy_url.connect_activate(|this, _| { + Display::default() .unwrap() .clipboard() .set_text(&this.state().unwrap().get::().unwrap()) }); + let action_link_copy_text = + SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); + action_link_copy_text.connect_activate(|this, _| { + Display::default() + .unwrap() + .clipboard() + .set_text(&this.state().unwrap().get::().unwrap()) + }); + let action_link_copy_text_selected = + SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); + action_link_copy_text_selected.connect_activate(|this, _| { + Display::default() + .unwrap() + .clipboard() + .set_text(&this.state().unwrap().get::().unwrap()) + }); + let action_link_bookmark = + SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); + action_link_bookmark.connect_activate({ + let p = profile.clone(); + move |this, _| { + let state = this.state().unwrap().get::().unwrap(); + p.bookmark.toggle(&state, None).unwrap(); + } + }); let action_link_download = SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); action_link_download.connect_activate({ @@ -130,14 +156,17 @@ impl Markdown { Some(&{ let g = SimpleActionGroup::new(); g.add_action(&action_link_tab); - g.add_action(&action_link_copy); + g.add_action(&action_link_copy_url); + g.add_action(&action_link_copy_text); + g.add_action(&action_link_copy_text_selected); + g.add_action(&action_link_bookmark); g.add_action(&action_link_download); g.add_action(&action_link_source); g }), ); let link_context = gtk::PopoverMenu::from_model(Some(&{ - let m = gtk::gio::Menu::new(); + let m = Menu::new(); m.append( Some("Open Link in New Tab"), Some(&format!( @@ -145,27 +174,56 @@ impl Markdown { action_link_tab.name() )), ); - m.append( - Some("Copy Link"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_copy.name() - )), - ); - m.append( - Some("Download Link"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_download.name() - )), - ); - m.append( - Some("View Link as Source"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_source.name() - )), - ); + m.append_section(None, &{ + let m_copy = Menu::new(); + m_copy.append( + Some("Copy Link URL"), + Some(&format!( + "{link_context_group_id}.{}", + action_link_copy_url.name() + )), + ); + m_copy.append( + Some("Copy Link Text"), + Some(&format!( + "{link_context_group_id}.{}", + action_link_copy_text.name() + )), + ); + m_copy.append( + Some("Copy Link Text Selected"), + Some(&format!( + "{link_context_group_id}.{}", + action_link_copy_text_selected.name() + )), + ); + m_copy + }); + m.append_section(None, &{ + let m_other = Menu::new(); + m_other.append( + Some("Bookmark Link"), // @TODO highlight state + Some(&format!( + "{link_context_group_id}.{}", + action_link_bookmark.name() + )), + ); + m_other.append( + Some("Download Link"), + Some(&format!( + "{link_context_group_id}.{}", + action_link_download.name() + )), + ); + m_other.append( + Some("View Link as Source"), + Some(&format!( + "{link_context_group_id}.{}", + action_link_source.name() + )), + ); + m_other + }); m })); link_context.set_parent(&text_view); @@ -231,18 +289,61 @@ impl Markdown { let request_str = uri.to_str(); let request_var = request_str.to_variant(); + // Open in the new tab action_link_tab.set_state(&request_var); - action_link_copy.set_state(&request_var); + action_link_copy_text.set_enabled(!request_str.is_empty()); + action_link_copy_url.set_state(&request_var); + action_link_copy_text.set_enabled(!request_str.is_empty()); + + { + // Copy link text + let mut start_iter = iter; + let mut end_iter = iter; + if !start_iter.starts_tag(Some(&tag)) { + start_iter.backward_to_tag_toggle(Some(&tag)); + } + if !end_iter.ends_tag(Some(&tag)) { + end_iter.forward_to_tag_toggle(Some(&tag)); + } + let tagged_text = text_view + .buffer() + .text(&start_iter, &end_iter, false) + .replace(LINK_EXTERNAL_INDICATOR, "") + .trim() + .to_string(); + + action_link_copy_text.set_state(&tagged_text.to_variant()); + action_link_copy_text.set_enabled(!tagged_text.is_empty()); + } + + // Copy link text (if) selected + if let Some((sel_start, sel_end)) = buffer.selection_bounds() { + let selected_tag_text = buffer.text(&sel_start, &sel_end, false); + action_link_copy_text_selected + .set_state(&selected_tag_text.to_variant()); + action_link_copy_text_selected + .set_enabled(!selected_tag_text.is_empty()); + } else { + action_link_copy_text_selected.set_enabled(false); + } + + // Bookmark + action_link_bookmark.set_state(&request_var); + action_link_bookmark.set_enabled(is_prefixable_link(&request_str)); + + // Download (new tab) action_link_download.set_state(&request_var); action_link_download.set_enabled(is_prefixable_link(&request_str)); + // View as Source (new tab) action_link_source.set_state(&request_var); action_link_source.set_enabled(is_prefixable_link(&request_str)); + // Toggle link_context .set_pointing_to(Some(>k::gdk::Rectangle::new(x, y, 1, 1))); - link_context.popup(); + link_context.popup() } } } @@ -405,5 +506,6 @@ fn link_prefix(request: String, prefix: &str) -> String { format!("{prefix}{}", request.trim_start_matches(prefix)) } +const LINK_EXTERNAL_INDICATOR: &str = "⇖"; const LINK_PREFIX_DOWNLOAD: &str = "download:"; const LINK_PREFIX_SOURCE: &str = "source:";