Skip to content

Commit bb08b7c

Browse files
author
yggverse
committed
implement copy link text, selected text, add link to the bookmarks (context menu) items; group menu items
1 parent c95cb6e commit bb08b7c

7 files changed

Lines changed: 295 additions & 77 deletions

File tree

src/app/browser/window/tab/item/client/driver/file/text.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ impl Text {
2121
.info
2222
.borrow_mut()
2323
.set_mime(Some("text/gemini".to_string()));
24-
page.content.to_text_gemini(uri, data)
24+
page.content.to_text_gemini(&page.profile, uri, data)
2525
}),
2626
Self::Markdown(uri, data) => (uri, {
2727
page.navigation
2828
.request
2929
.info
3030
.borrow_mut()
3131
.set_mime(Some("text/markdown".to_string()));
32-
page.content.to_text_markdown(uri, data)
32+
page.content.to_text_markdown(&page.profile, uri, data)
3333
}),
3434
Self::Plain(uri, data) => (uri, page.content.to_text_plain(data)),
3535
Self::Source(uri, data) => (uri, page.content.to_text_source(data)),

src/app/browser/window/tab/item/client/driver/gemini.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,8 @@ fn handle(
357357
page.content.to_text_source(data)
358358
} else {
359359
match m.as_str() {
360-
"text/gemini" => page.content.to_text_gemini(&uri, data),
361-
"text/markdown" => page.content.to_text_markdown(&uri, data),
360+
"text/gemini" => page.content.to_text_gemini(&page.profile, &uri, data),
361+
"text/markdown" => page.content.to_text_markdown(&page.profile, &uri, data),
362362
"text/plain" => page.content.to_text_plain(data),
363363
_ => panic!() // unexpected
364364
}

src/app/browser/window/tab/item/client/driver/nex.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ fn render(
299299
} else if q.ends_with("/") {
300300
p.content.to_text_nex(&u, d)
301301
} else if q.ends_with(".gmi") || q.ends_with(".gemini") {
302-
p.content.to_text_gemini(&u, d)
302+
p.content.to_text_gemini(&p.profile, &u, d)
303303
} else {
304304
p.content.to_text_plain(d)
305305
};

src/app/browser/window/tab/item/page/content.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use directory::Directory;
77
use image::Image;
88
use text::Text;
99

10+
use crate::profile::Profile;
11+
1012
use super::{ItemAction, TabAction, WindowAction};
1113
use adw::StatusPage;
1214
use gtk::{
@@ -126,9 +128,14 @@ impl Content {
126128
}
127129

128130
/// `text/gemini`
129-
pub fn to_text_gemini(&self, base: &Uri, data: &str) -> Text {
131+
pub fn to_text_gemini(&self, profile: &Rc<Profile>, base: &Uri, data: &str) -> Text {
130132
self.clean();
131-
match Text::gemini((&self.window_action, &self.item_action), base, data) {
133+
match Text::gemini(
134+
(&self.window_action, &self.item_action),
135+
profile,
136+
base,
137+
data,
138+
) {
132139
Ok(text) => {
133140
self.g_box.append(&text.scrolled_window);
134141
text
@@ -155,9 +162,14 @@ impl Content {
155162
}
156163

157164
/// `text/markdown`
158-
pub fn to_text_markdown(&self, base: &Uri, data: &str) -> Text {
165+
pub fn to_text_markdown(&self, profile: &Rc<Profile>, base: &Uri, data: &str) -> Text {
159166
self.clean();
160-
let m = Text::markdown((&self.window_action, &self.item_action), base, data);
167+
let m = Text::markdown(
168+
(&self.window_action, &self.item_action),
169+
profile,
170+
base,
171+
data,
172+
);
161173
self.g_box.append(&m.scrolled_window);
162174
m
163175
}

src/app/browser/window/tab/item/page/content/text.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ mod nex;
44
mod plain;
55
mod source;
66

7+
use crate::profile::Profile;
8+
79
use super::{ItemAction, WindowAction};
810
use adw::ClampScrollable;
911
use gemini::Gemini;
@@ -27,10 +29,11 @@ pub struct Text {
2729
impl Text {
2830
pub fn gemini(
2931
actions: (&Rc<WindowAction>, &Rc<ItemAction>),
32+
profile: &Rc<Profile>,
3033
base: &Uri,
3134
gemtext: &str,
3235
) -> Result<Self, (String, Option<Self>)> {
33-
match Gemini::build(actions, base, gemtext) {
36+
match Gemini::build(actions, profile, base, gemtext) {
3437
Ok(widget) => Ok(Self {
3538
scrolled_window: reader(&widget.text_view),
3639
text_view: widget.text_view,
@@ -55,10 +58,11 @@ impl Text {
5558

5659
pub fn markdown(
5760
actions: (&Rc<WindowAction>, &Rc<ItemAction>),
61+
profile: &Rc<Profile>,
5862
base: &Uri,
5963
gemtext: &str,
6064
) -> Self {
61-
let markdown = Markdown::build(actions, base, gemtext);
65+
let markdown = Markdown::build(actions, profile, base, gemtext);
6266
Self {
6367
scrolled_window: reader(&markdown.text_view),
6468
text_view: markdown.text_view,

src/app/browser/window/tab/item/page/content/text/gemini.rs

Lines changed: 135 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ mod syntax;
66
mod tag;
77

88
use super::{ItemAction, WindowAction};
9-
use crate::app::browser::window::action::Position;
9+
use crate::{app::browser::window::action::Position, profile::Profile};
1010
pub use error::Error;
1111
use gtk::{
1212
EventControllerMotion, GestureClick, TextBuffer, TextTag, TextView, TextWindowType,
1313
UriLauncher, Window, WrapMode,
14-
gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, BUTTON_SECONDARY, RGBA},
15-
gio::{Cancellable, SimpleAction, SimpleActionGroup},
14+
gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, BUTTON_SECONDARY, Display, RGBA},
15+
gio::{Cancellable, Menu, SimpleAction, SimpleActionGroup},
1616
glib::{Uri, uuid_string_random},
1717
prelude::{PopoverExt, TextBufferExt, TextBufferExtManual, TextTagExt, TextViewExt, WidgetExt},
1818
};
@@ -36,6 +36,7 @@ impl Gemini {
3636
/// Build new `Self`
3737
pub fn build(
3838
(window_action, item_action): (&Rc<WindowAction>, &Rc<ItemAction>),
39+
profile: &Rc<Profile>,
3940
base: &Uri,
4041
gemtext: &str,
4142
) -> Result<Self, Error> {
@@ -220,7 +221,7 @@ impl Gemini {
220221
let mut alt = Vec::with_capacity(2);
221222

222223
if uri.scheme() != base.scheme() {
223-
alt.push("⇖".to_string());
224+
alt.push(LINK_EXTERNAL_INDICATOR.to_string());
224225
}
225226

226227
alt.push(match link.alt {
@@ -235,9 +236,7 @@ impl Gemini {
235236
.wrap_mode(WrapMode::Word)
236237
.build();
237238

238-
if !tag.text_tag_table.add(&a) {
239-
panic!()
240-
}
239+
assert!(tag.text_tag_table.add(&a));
241240

242241
buffer.insert_with_tags(&mut buffer.end_iter(), &alt.join(" "), &[&a]);
243242
buffer.insert(&mut buffer.end_iter(), NEW_LINE);
@@ -296,14 +295,39 @@ impl Gemini {
296295
)
297296
}
298297
});
299-
let action_link_copy =
298+
let action_link_copy_url =
299+
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
300+
action_link_copy_url.connect_activate(|this, _| {
301+
Display::default()
302+
.unwrap()
303+
.clipboard()
304+
.set_text(&this.state().unwrap().get::<String>().unwrap())
305+
});
306+
let action_link_copy_text =
307+
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
308+
action_link_copy_text.connect_activate(|this, _| {
309+
Display::default()
310+
.unwrap()
311+
.clipboard()
312+
.set_text(&this.state().unwrap().get::<String>().unwrap())
313+
});
314+
let action_link_copy_text_selected =
300315
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
301-
action_link_copy.connect_activate(|this, _| {
302-
gtk::gdk::Display::default()
316+
action_link_copy_text_selected.connect_activate(|this, _| {
317+
Display::default()
303318
.unwrap()
304319
.clipboard()
305320
.set_text(&this.state().unwrap().get::<String>().unwrap())
306321
});
322+
let action_link_bookmark =
323+
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
324+
action_link_bookmark.connect_activate({
325+
let p = profile.clone();
326+
move |this, _| {
327+
let state = this.state().unwrap().get::<String>().unwrap();
328+
p.bookmark.toggle(&state, None).unwrap();
329+
}
330+
});
307331
let action_link_download =
308332
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
309333
action_link_download.connect_activate({
@@ -338,42 +362,74 @@ impl Gemini {
338362
Some(&{
339363
let g = SimpleActionGroup::new();
340364
g.add_action(&action_link_tab);
341-
g.add_action(&action_link_copy);
365+
g.add_action(&action_link_copy_url);
366+
g.add_action(&action_link_copy_text);
367+
g.add_action(&action_link_copy_text_selected);
368+
g.add_action(&action_link_bookmark);
342369
g.add_action(&action_link_download);
343370
g.add_action(&action_link_source);
344371
g
345372
}),
346373
);
347374
let link_context = gtk::PopoverMenu::from_model(Some(&{
348-
let m = gtk::gio::Menu::new();
375+
let m = Menu::new();
349376
m.append(
350377
Some("Open Link in New Tab"),
351378
Some(&format!(
352379
"{link_context_group_id}.{}",
353380
action_link_tab.name()
354381
)),
355382
);
356-
m.append(
357-
Some("Copy Link"),
358-
Some(&format!(
359-
"{link_context_group_id}.{}",
360-
action_link_copy.name()
361-
)),
362-
);
363-
m.append(
364-
Some("Download Link"),
365-
Some(&format!(
366-
"{link_context_group_id}.{}",
367-
action_link_download.name()
368-
)),
369-
);
370-
m.append(
371-
Some("View Link as Source"),
372-
Some(&format!(
373-
"{link_context_group_id}.{}",
374-
action_link_source.name()
375-
)),
376-
);
383+
m.append_section(None, &{
384+
let m_copy = Menu::new();
385+
m_copy.append(
386+
Some("Copy Link URL"),
387+
Some(&format!(
388+
"{link_context_group_id}.{}",
389+
action_link_copy_url.name()
390+
)),
391+
);
392+
m_copy.append(
393+
Some("Copy Link Text"),
394+
Some(&format!(
395+
"{link_context_group_id}.{}",
396+
action_link_copy_text.name()
397+
)),
398+
);
399+
m_copy.append(
400+
Some("Copy Link Text Selected"),
401+
Some(&format!(
402+
"{link_context_group_id}.{}",
403+
action_link_copy_text_selected.name()
404+
)),
405+
);
406+
m_copy
407+
});
408+
m.append_section(None, &{
409+
let m_other = Menu::new();
410+
m_other.append(
411+
Some("Bookmark Link"), // @TODO highlight state
412+
Some(&format!(
413+
"{link_context_group_id}.{}",
414+
action_link_bookmark.name()
415+
)),
416+
);
417+
m_other.append(
418+
Some("Download Link"),
419+
Some(&format!(
420+
"{link_context_group_id}.{}",
421+
action_link_download.name()
422+
)),
423+
);
424+
m_other.append(
425+
Some("View Link as Source"),
426+
Some(&format!(
427+
"{link_context_group_id}.{}",
428+
action_link_source.name()
429+
)),
430+
);
431+
m_other
432+
});
377433
m
378434
}));
379435
link_context.set_parent(&text_view);
@@ -435,18 +491,61 @@ impl Gemini {
435491
let request_str = uri.to_str();
436492
let request_var = request_str.to_variant();
437493

494+
// Open in the new tab
438495
action_link_tab.set_state(&request_var);
439-
action_link_copy.set_state(&request_var);
496+
action_link_copy_text.set_enabled(!request_str.is_empty());
497+
498+
action_link_copy_url.set_state(&request_var);
499+
action_link_copy_text.set_enabled(!request_str.is_empty());
500+
501+
{
502+
// Copy link text
503+
let mut start_iter = iter;
504+
let mut end_iter = iter;
505+
if !start_iter.starts_tag(Some(&tag)) {
506+
start_iter.backward_to_tag_toggle(Some(&tag));
507+
}
508+
if !end_iter.ends_tag(Some(&tag)) {
509+
end_iter.forward_to_tag_toggle(Some(&tag));
510+
}
511+
let tagged_text = text_view
512+
.buffer()
513+
.text(&start_iter, &end_iter, false)
514+
.replace(LINK_EXTERNAL_INDICATOR, "")
515+
.trim()
516+
.to_string();
517+
518+
action_link_copy_text.set_state(&tagged_text.to_variant());
519+
action_link_copy_text.set_enabled(!tagged_text.is_empty());
520+
}
521+
522+
// Copy link text (if) selected
523+
if let Some((sel_start, sel_end)) = buffer.selection_bounds() {
524+
let selected_tag_text = buffer.text(&sel_start, &sel_end, false);
525+
action_link_copy_text_selected
526+
.set_state(&selected_tag_text.to_variant());
527+
action_link_copy_text_selected
528+
.set_enabled(!selected_tag_text.is_empty());
529+
} else {
530+
action_link_copy_text_selected.set_enabled(false);
531+
}
532+
533+
// Bookmark
534+
action_link_bookmark.set_state(&request_var);
535+
action_link_bookmark.set_enabled(is_prefixable_link(&request_str));
440536

537+
// Download (new tab)
441538
action_link_download.set_state(&request_var);
442539
action_link_download.set_enabled(is_prefixable_link(&request_str));
443540

541+
// View as Source (new tab)
444542
action_link_source.set_state(&request_var);
445543
action_link_source.set_enabled(is_prefixable_link(&request_str));
446544

545+
// Toggle
447546
link_context
448547
.set_pointing_to(Some(&gtk::gdk::Rectangle::new(x, y, 1, 1)));
449-
link_context.popup();
548+
link_context.popup()
450549
}
451550
}
452551
}
@@ -580,5 +679,6 @@ fn link_prefix(request: String, prefix: &str) -> String {
580679
format!("{prefix}{}", request.trim_start_matches(prefix))
581680
}
582681

682+
const LINK_EXTERNAL_INDICATOR: &str = "⇖";
583683
const LINK_PREFIX_DOWNLOAD: &str = "download:";
584684
const LINK_PREFIX_SOURCE: &str = "source:";

0 commit comments

Comments
 (0)