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 51d5abc8..9ffa3856 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 @@ -1,10 +1,12 @@ mod ansi; pub mod error; +mod gutter; mod icon; mod syntax; mod tag; pub use error::Error; +use gutter::Gutter; use icon::Icon; use syntax::Syntax; use tag::Tag; @@ -98,6 +100,9 @@ impl Gemini { .build() }; + // Init gutter widget (the tooltip on URL tags hover) + let gutter = Gutter::build(&text_view); + // Parse gemtext lines for line in gemtext.lines() { // Is inline code @@ -375,8 +380,8 @@ impl Gemini { Window::NONE, Cancellable::NONE, |result| { - if let Err(error) = result { - println!("{error}") + if let Err(e) = result { + println!("{e}") } }, ), @@ -460,12 +465,12 @@ impl Gemini { // Keep hovered tag in memory hover.replace(Some(tag.clone())); + // Show tooltip + gutter.set_uri(Some(uri)); + // Toggle cursor text_view.set_cursor_from_name(Some("pointer")); - // Show tooltip | @TODO set_gutter option? - text_view.set_tooltip_text(Some(&uri.to_string())); - // Redraw required to apply changes immediately text_view.queue_draw(); @@ -475,8 +480,8 @@ impl Gemini { } // Restore defaults + gutter.set_uri(None); text_view.set_cursor_from_name(Some("text")); - text_view.set_tooltip_text(None); text_view.queue_draw(); } }); // @TODO may be expensive for CPU, add timeout? diff --git a/src/app/browser/window/tab/item/page/content/text/gemini/gutter.rs b/src/app/browser/window/tab/item/page/content/text/gemini/gutter.rs new file mode 100644 index 00000000..ee4ae064 --- /dev/null +++ b/src/app/browser/window/tab/item/page/content/text/gemini/gutter.rs @@ -0,0 +1,70 @@ +use gtk::{ + glib::{timeout_add_local_once, Uri}, + pango::EllipsizeMode, + prelude::{TextViewExt, WidgetExt}, + Align, Label, TextView, TextWindowType, +}; +use std::{cell::Cell, rc::Rc, time::Duration}; + +pub struct Gutter { + pub label: Label, + is_active: Rc>, +} + +impl Gutter { + pub fn build(text_view: &TextView) -> Self { + const MARGIN_X: i32 = 8; + const MARGIN_Y: i32 = 2; + let label = Label::builder() + .css_classes([ + // "caption", + "dim-label", + ]) + .halign(Align::Start) + .margin_start(MARGIN_X) + .margin_end(MARGIN_X) + .margin_top(MARGIN_Y) + .margin_bottom(MARGIN_Y) + .ellipsize(EllipsizeMode::Middle) + .build(); + + text_view.set_gutter(TextWindowType::Bottom, Some(&label)); + text_view + .gutter(TextWindowType::Bottom) + .unwrap() + .set_css_classes(&["view"]); // @TODO unspecified patch + + Self { + is_active: Rc::new(Cell::new(false)), + label, + } + } + + pub fn set_uri(&self, uri: Option<&Uri>) { + match uri { + Some(uri) => { + if !self.label.is_visible() { + if !self.is_active.replace(true) { + timeout_add_local_once(Duration::from_millis(500), { + let label = self.label.clone(); + let is_active = self.is_active.clone(); + let uri = uri.clone(); + move || { + if is_active.replace(false) { + label.set_label(&uri.to_string()); + label.set_visible(true) + } + } + }); + } + } else { + self.label.set_label(&uri.to_string()) + } + } + None => { + self.is_active.replace(false); + self.label.set_visible(false) + } + } + } +}