mirror of
https://github.com/YGGverse/Yoda.git
synced 2026-03-31 16:45:27 +00:00
init text/markdown parser (based on text/gemini)
This commit is contained in:
parent
6fb7e70213
commit
fc6cce8072
23 changed files with 1452 additions and 0 deletions
|
|
@ -94,6 +94,18 @@ impl File {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else if url.ends_with(".md") || url.ends_with(".markdown")
|
||||||
|
{
|
||||||
|
load_contents_async(file, cancellable, move |result| {
|
||||||
|
match result {
|
||||||
|
Ok(data) => {
|
||||||
|
Text::Markdown(uri, data).handle(&page)
|
||||||
|
}
|
||||||
|
Err(message) => {
|
||||||
|
Status::Failure(message).handle(&page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
load_contents_async(file, cancellable, move |result| {
|
load_contents_async(file, cancellable, move |result| {
|
||||||
match result {
|
match result {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use gtk::glib::Uri;
|
||||||
|
|
||||||
pub enum Text {
|
pub enum Text {
|
||||||
Gemini(Uri, String),
|
Gemini(Uri, String),
|
||||||
|
Markdown(Uri, String),
|
||||||
Plain(Uri, String),
|
Plain(Uri, String),
|
||||||
Source(Uri, String),
|
Source(Uri, String),
|
||||||
}
|
}
|
||||||
|
|
@ -22,6 +23,14 @@ impl Text {
|
||||||
.set_mime(Some("text/gemini".to_string()));
|
.set_mime(Some("text/gemini".to_string()));
|
||||||
page.content.to_text_gemini(uri, data)
|
page.content.to_text_gemini(uri, data)
|
||||||
}),
|
}),
|
||||||
|
Self::Markdown(uri, data) => (uri, {
|
||||||
|
page.navigation
|
||||||
|
.request
|
||||||
|
.info
|
||||||
|
.borrow_mut()
|
||||||
|
.set_mime(Some("text/markdown".to_string()));
|
||||||
|
page.content.to_text_markdown(uri, data)
|
||||||
|
}),
|
||||||
Self::Plain(uri, data) => (uri, page.content.to_text_plain(data)),
|
Self::Plain(uri, data) => (uri, page.content.to_text_plain(data)),
|
||||||
Self::Source(uri, data) => (uri, page.content.to_text_source(data)),
|
Self::Source(uri, data) => (uri, page.content.to_text_source(data)),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -358,6 +358,7 @@ fn handle(
|
||||||
} else {
|
} else {
|
||||||
match m.as_str() {
|
match m.as_str() {
|
||||||
"text/gemini" => page.content.to_text_gemini(&uri, data),
|
"text/gemini" => page.content.to_text_gemini(&uri, data),
|
||||||
|
"text/markdown" => page.content.to_text_markdown(&uri, data),
|
||||||
"text/plain" => page.content.to_text_plain(data),
|
"text/plain" => page.content.to_text_plain(data),
|
||||||
_ => panic!() // unexpected
|
_ => panic!() // unexpected
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,35 @@ impl Content {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `text/markdown`
|
||||||
|
pub fn to_text_markdown(&self, base: &Uri, data: &str) -> Text {
|
||||||
|
self.clean();
|
||||||
|
match Text::markdown((&self.window_action, &self.item_action), base, data) {
|
||||||
|
Ok(text) => {
|
||||||
|
self.g_box.append(&text.scrolled_window);
|
||||||
|
text
|
||||||
|
}
|
||||||
|
Err((message, text)) => {
|
||||||
|
self.g_box.append(&{
|
||||||
|
let banner = adw::Banner::builder()
|
||||||
|
.title(message)
|
||||||
|
.revealed(true)
|
||||||
|
.button_label("Ok")
|
||||||
|
.build();
|
||||||
|
banner.connect_button_clicked(|this| this.set_revealed(false));
|
||||||
|
banner
|
||||||
|
});
|
||||||
|
match text {
|
||||||
|
Some(text) => {
|
||||||
|
self.g_box.append(&text.scrolled_window);
|
||||||
|
text
|
||||||
|
}
|
||||||
|
None => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `text/plain`
|
/// `text/plain`
|
||||||
pub fn to_text_plain(&self, data: &str) -> Text {
|
pub fn to_text_plain(&self, data: &str) -> Text {
|
||||||
self.clean();
|
self.clean();
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ impl Format for FileInfo {
|
||||||
if content_type == "text/plain" {
|
if content_type == "text/plain" {
|
||||||
if display_name.ends_with(".gmi") || display_name.ends_with(".gemini") {
|
if display_name.ends_with(".gmi") || display_name.ends_with(".gemini") {
|
||||||
"text/gemini".into()
|
"text/gemini".into()
|
||||||
|
} else if display_name.ends_with(".md") || display_name.ends_with(".markdown") {
|
||||||
|
"text/markdown".into()
|
||||||
} else {
|
} else {
|
||||||
content_type
|
content_type
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
mod gemini;
|
mod gemini;
|
||||||
|
mod markdown;
|
||||||
mod nex;
|
mod nex;
|
||||||
mod plain;
|
mod plain;
|
||||||
mod source;
|
mod source;
|
||||||
|
|
@ -7,6 +8,7 @@ use super::{ItemAction, WindowAction};
|
||||||
use adw::ClampScrollable;
|
use adw::ClampScrollable;
|
||||||
use gemini::Gemini;
|
use gemini::Gemini;
|
||||||
use gtk::{ScrolledWindow, TextView, glib::Uri};
|
use gtk::{ScrolledWindow, TextView, glib::Uri};
|
||||||
|
use markdown::Markdown;
|
||||||
use nex::Nex;
|
use nex::Nex;
|
||||||
use plain::Plain;
|
use plain::Plain;
|
||||||
use source::Source;
|
use source::Source;
|
||||||
|
|
@ -51,6 +53,34 @@ impl Text {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn markdown(
|
||||||
|
actions: (&Rc<WindowAction>, &Rc<ItemAction>),
|
||||||
|
base: &Uri,
|
||||||
|
gemtext: &str,
|
||||||
|
) -> Result<Self, (String, Option<Self>)> {
|
||||||
|
match Markdown::build(actions, base, gemtext) {
|
||||||
|
Ok(widget) => Ok(Self {
|
||||||
|
scrolled_window: reader(&widget.text_view),
|
||||||
|
text_view: widget.text_view,
|
||||||
|
meta: Meta {
|
||||||
|
title: widget.title,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Err(e) => match e {
|
||||||
|
markdown::Error::Markup(message, widget) => Err((
|
||||||
|
message,
|
||||||
|
Some(Self {
|
||||||
|
scrolled_window: reader(&widget.text_view),
|
||||||
|
text_view: widget.text_view,
|
||||||
|
meta: Meta {
|
||||||
|
title: widget.title,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn plain(data: &str) -> Self {
|
pub fn plain(data: &str) -> Self {
|
||||||
let text_view = TextView::plain(data);
|
let text_view = TextView::plain(data);
|
||||||
Self {
|
Self {
|
||||||
|
|
|
||||||
584
src/app/browser/window/tab/item/page/content/text/markdown.rs
Normal file
584
src/app/browser/window/tab/item/page/content/text/markdown.rs
Normal file
|
|
@ -0,0 +1,584 @@
|
||||||
|
mod ansi;
|
||||||
|
pub mod error;
|
||||||
|
mod gutter;
|
||||||
|
mod icon;
|
||||||
|
mod syntax;
|
||||||
|
mod tag;
|
||||||
|
|
||||||
|
use super::{ItemAction, WindowAction};
|
||||||
|
use crate::app::browser::window::action::Position;
|
||||||
|
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},
|
||||||
|
glib::{Uri, uuid_string_random},
|
||||||
|
prelude::{PopoverExt, TextBufferExt, TextBufferExtManual, TextTagExt, TextViewExt, WidgetExt},
|
||||||
|
};
|
||||||
|
use gutter::Gutter;
|
||||||
|
use icon::Icon;
|
||||||
|
use sourceview::prelude::{ActionExt, ActionMapExt, DisplayExt, ToVariant};
|
||||||
|
use std::{cell::Cell, collections::HashMap, rc::Rc};
|
||||||
|
use syntax::Syntax;
|
||||||
|
use tag::Tag;
|
||||||
|
|
||||||
|
pub const NEW_LINE: &str = "\n";
|
||||||
|
|
||||||
|
pub struct Markdown {
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub text_view: TextView,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Markdown {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// Build new `Self`
|
||||||
|
pub fn build(
|
||||||
|
(window_action, item_action): (&Rc<WindowAction>, &Rc<ItemAction>),
|
||||||
|
base: &Uri,
|
||||||
|
gemtext: &str,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
// Init default values
|
||||||
|
let mut title = None;
|
||||||
|
|
||||||
|
// Init HashMap storage (for event controllers)
|
||||||
|
let mut links: HashMap<TextTag, Uri> = HashMap::new();
|
||||||
|
|
||||||
|
// Init hovered tag storage for `links`
|
||||||
|
// * maybe less expensive than update entire HashMap by iter
|
||||||
|
let hover: Rc<Cell<Option<TextTag>>> = Rc::new(Cell::new(None));
|
||||||
|
|
||||||
|
// Init code features
|
||||||
|
let mut code = None;
|
||||||
|
|
||||||
|
// Init quote icon feature
|
||||||
|
let mut is_line_after_quote = false;
|
||||||
|
|
||||||
|
// Init colors
|
||||||
|
// @TODO use accent colors in adw 1.6 / ubuntu 24.10+
|
||||||
|
let link_color = (
|
||||||
|
RGBA::new(0.208, 0.518, 0.894, 1.0),
|
||||||
|
RGBA::new(0.208, 0.518, 0.894, 0.9),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Init syntect highlight features
|
||||||
|
let syntax = Syntax::new();
|
||||||
|
|
||||||
|
// Init icons
|
||||||
|
let icon = Icon::new();
|
||||||
|
|
||||||
|
// Init tags
|
||||||
|
let tag = Tag::new();
|
||||||
|
|
||||||
|
// Init new text buffer
|
||||||
|
let buffer = TextBuffer::new(Some(&tag.text_tag_table));
|
||||||
|
|
||||||
|
// Init main widget
|
||||||
|
let text_view = {
|
||||||
|
const MARGIN: i32 = 8;
|
||||||
|
TextView::builder()
|
||||||
|
.bottom_margin(MARGIN)
|
||||||
|
.buffer(&buffer)
|
||||||
|
.cursor_visible(false)
|
||||||
|
.editable(false)
|
||||||
|
.left_margin(MARGIN)
|
||||||
|
.right_margin(MARGIN)
|
||||||
|
.top_margin(MARGIN)
|
||||||
|
.vexpand(true)
|
||||||
|
.wrap_mode(WrapMode::Word)
|
||||||
|
.build()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Init gutter widget (the tooltip on URL tags hover)
|
||||||
|
let gutter = Gutter::build(&text_view);
|
||||||
|
|
||||||
|
// Disable code format on at least one closing tag not found
|
||||||
|
// gemini://bbs.geminispace.org/s/Gemini/26031
|
||||||
|
let is_code_enabled = {
|
||||||
|
use ggemtext::line::code::{self};
|
||||||
|
let mut t: usize = 0;
|
||||||
|
for l in gemtext.lines() {
|
||||||
|
if l.starts_with(code::TAG) {
|
||||||
|
t += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t == 0 || t.is_multiple_of(2)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse gemtext lines
|
||||||
|
for line in gemtext.lines() {
|
||||||
|
if is_code_enabled {
|
||||||
|
use ggemtext::line::Code;
|
||||||
|
match code {
|
||||||
|
None => {
|
||||||
|
// Open tag found
|
||||||
|
if let Some(c) = Code::begin_from(line) {
|
||||||
|
// Begin next lines collection into the code buffer
|
||||||
|
code = Some(c);
|
||||||
|
|
||||||
|
// Skip other actions for this line
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(ref mut c) => {
|
||||||
|
match c.continue_from(line) {
|
||||||
|
Ok(()) => {
|
||||||
|
// Close tag found:
|
||||||
|
if c.is_completed {
|
||||||
|
// Is alt provided
|
||||||
|
let alt = match c.alt {
|
||||||
|
Some(ref alt) => {
|
||||||
|
// Insert alt value to the main buffer
|
||||||
|
buffer.insert_with_tags(
|
||||||
|
&mut buffer.end_iter(),
|
||||||
|
alt.as_str(),
|
||||||
|
&[&tag.title],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Append new line after alt text
|
||||||
|
buffer.insert(&mut buffer.end_iter(), NEW_LINE);
|
||||||
|
|
||||||
|
// Return value as wanted also for syntax highlight detection
|
||||||
|
Some(alt)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Begin code block construction
|
||||||
|
// Try auto-detect code syntax for given `value` and `alt` @TODO optional
|
||||||
|
match syntax.highlight(&c.value, alt) {
|
||||||
|
Ok(highlight) => {
|
||||||
|
for (syntax_tag, entity) in highlight {
|
||||||
|
// Register new tag
|
||||||
|
if !tag.text_tag_table.add(&syntax_tag) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
// Append tag to buffer
|
||||||
|
buffer.insert_with_tags(
|
||||||
|
&mut buffer.end_iter(),
|
||||||
|
&entity,
|
||||||
|
&[&syntax_tag],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Try ANSI/SGR format (terminal emulation) @TODO optional
|
||||||
|
for (syntax_tag, entity) in ansi::format(&c.value) {
|
||||||
|
// Register new tag
|
||||||
|
if !tag.text_tag_table.add(&syntax_tag) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
// Append tag to buffer
|
||||||
|
buffer.insert_with_tags(
|
||||||
|
&mut buffer.end_iter(),
|
||||||
|
&entity,
|
||||||
|
&[&syntax_tag],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} // @TODO handle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
code = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip other actions for this line
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(_) => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is header
|
||||||
|
{
|
||||||
|
use ggemtext::line::{Header, header::Level};
|
||||||
|
if let Some(header) = Header::parse(line) {
|
||||||
|
buffer.insert_with_tags(
|
||||||
|
&mut buffer.end_iter(),
|
||||||
|
&header.value,
|
||||||
|
&[match header.level {
|
||||||
|
Level::H1 => &tag.h1,
|
||||||
|
Level::H2 => &tag.h2,
|
||||||
|
Level::H3 => &tag.h3,
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
buffer.insert(&mut buffer.end_iter(), NEW_LINE);
|
||||||
|
|
||||||
|
if title.is_none() {
|
||||||
|
title = Some(header.value.clone());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is link
|
||||||
|
if let Some(link) = ggemtext::line::Link::parse(line) {
|
||||||
|
if let Some(uri) = link.uri(Some(base)) {
|
||||||
|
let mut alt = Vec::new();
|
||||||
|
|
||||||
|
if uri.scheme() != base.scheme() {
|
||||||
|
alt.push("⇖".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
alt.push(match link.alt {
|
||||||
|
Some(alt) => alt,
|
||||||
|
None => uri.to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let a = TextTag::builder()
|
||||||
|
.foreground_rgba(&link_color.0)
|
||||||
|
// .foreground_rgba(&adw::StyleManager::default().accent_color_rgba()) @TODO adw 1.6 / ubuntu 24.10+
|
||||||
|
.sentence(true)
|
||||||
|
.wrap_mode(WrapMode::Word)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
if !tag.text_tag_table.add(&a) {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.insert_with_tags(&mut buffer.end_iter(), &alt.join(" "), &[&a]);
|
||||||
|
buffer.insert(&mut buffer.end_iter(), NEW_LINE);
|
||||||
|
|
||||||
|
links.insert(a, uri);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is list
|
||||||
|
|
||||||
|
if let Some(value) = ggemtext::line::list::Gemtext::as_value(line) {
|
||||||
|
buffer.insert_with_tags(
|
||||||
|
&mut buffer.end_iter(),
|
||||||
|
&format!("• {value}"),
|
||||||
|
&[&tag.list],
|
||||||
|
);
|
||||||
|
buffer.insert(&mut buffer.end_iter(), NEW_LINE);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is quote
|
||||||
|
|
||||||
|
if let Some(quote) = ggemtext::line::quote::Gemtext::as_value(line) {
|
||||||
|
// Show quote indicator if last line is not quote (to prevent duplicates)
|
||||||
|
if !is_line_after_quote {
|
||||||
|
// Show only if the icons resolved for default `Display`
|
||||||
|
if let Some(ref icon) = icon {
|
||||||
|
buffer.insert_paintable(&mut buffer.end_iter(), &icon.quote);
|
||||||
|
buffer.insert(&mut buffer.end_iter(), NEW_LINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer.insert_with_tags(&mut buffer.end_iter(), quote, &[&tag.quote]);
|
||||||
|
buffer.insert(&mut buffer.end_iter(), NEW_LINE);
|
||||||
|
is_line_after_quote = true;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
is_line_after_quote = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing match custom tags above,
|
||||||
|
// just append plain text covered in empty tag (to handle controller events properly)
|
||||||
|
buffer.insert_with_tags(&mut buffer.end_iter(), line, &[&tag.plain]);
|
||||||
|
buffer.insert(&mut buffer.end_iter(), NEW_LINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context menu
|
||||||
|
let action_link_tab =
|
||||||
|
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
|
||||||
|
action_link_tab.connect_activate({
|
||||||
|
let window_action = window_action.clone();
|
||||||
|
move |this, _| {
|
||||||
|
open_link_in_new_tab(
|
||||||
|
&this.state().unwrap().get::<String>().unwrap(),
|
||||||
|
&window_action,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let action_link_copy =
|
||||||
|
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
|
||||||
|
action_link_copy.connect_activate(|this, _| {
|
||||||
|
gtk::gdk::Display::default()
|
||||||
|
.unwrap()
|
||||||
|
.clipboard()
|
||||||
|
.set_text(&this.state().unwrap().get::<String>().unwrap())
|
||||||
|
});
|
||||||
|
let action_link_download =
|
||||||
|
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
|
||||||
|
action_link_download.connect_activate({
|
||||||
|
let window_action = window_action.clone();
|
||||||
|
move |this, _| {
|
||||||
|
open_link_in_new_tab(
|
||||||
|
&link_prefix(
|
||||||
|
this.state().unwrap().get::<String>().unwrap(),
|
||||||
|
LINK_PREFIX_DOWNLOAD,
|
||||||
|
),
|
||||||
|
&window_action,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let action_link_source =
|
||||||
|
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
|
||||||
|
action_link_source.connect_activate({
|
||||||
|
let window_action = window_action.clone();
|
||||||
|
move |this, _| {
|
||||||
|
open_link_in_new_tab(
|
||||||
|
&link_prefix(
|
||||||
|
this.state().unwrap().get::<String>().unwrap(),
|
||||||
|
LINK_PREFIX_SOURCE,
|
||||||
|
),
|
||||||
|
&window_action,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let link_context_group_id = uuid_string_random();
|
||||||
|
text_view.insert_action_group(
|
||||||
|
&link_context_group_id,
|
||||||
|
Some(&{
|
||||||
|
let g = SimpleActionGroup::new();
|
||||||
|
g.add_action(&action_link_tab);
|
||||||
|
g.add_action(&action_link_copy);
|
||||||
|
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();
|
||||||
|
m.append(
|
||||||
|
Some("Open Link in New Tab"),
|
||||||
|
Some(&format!(
|
||||||
|
"{link_context_group_id}.{}",
|
||||||
|
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
|
||||||
|
}));
|
||||||
|
link_context.set_parent(&text_view);
|
||||||
|
|
||||||
|
// Init additional controllers
|
||||||
|
let middle_button_controller = GestureClick::builder().button(BUTTON_MIDDLE).build();
|
||||||
|
let primary_button_controller = GestureClick::builder().button(BUTTON_PRIMARY).build();
|
||||||
|
let secondary_button_controller = GestureClick::builder()
|
||||||
|
.button(BUTTON_SECONDARY)
|
||||||
|
.propagation_phase(gtk::PropagationPhase::Capture)
|
||||||
|
.build();
|
||||||
|
let motion_controller = EventControllerMotion::new();
|
||||||
|
|
||||||
|
text_view.add_controller(middle_button_controller.clone());
|
||||||
|
text_view.add_controller(motion_controller.clone());
|
||||||
|
text_view.add_controller(primary_button_controller.clone());
|
||||||
|
text_view.add_controller(secondary_button_controller.clone());
|
||||||
|
|
||||||
|
// Init shared reference container for HashTable collected
|
||||||
|
let links = Rc::new(links);
|
||||||
|
|
||||||
|
// Init events
|
||||||
|
primary_button_controller.connect_released({
|
||||||
|
let item_action = item_action.clone();
|
||||||
|
let links = links.clone();
|
||||||
|
let text_view = text_view.clone();
|
||||||
|
move |_, _, window_x, window_y| {
|
||||||
|
// Detect tag match current coords hovered
|
||||||
|
let (buffer_x, buffer_y) = text_view.window_to_buffer_coords(
|
||||||
|
TextWindowType::Widget,
|
||||||
|
window_x as i32,
|
||||||
|
window_y as i32,
|
||||||
|
);
|
||||||
|
if let Some(iter) = text_view.iter_at_location(buffer_x, buffer_y) {
|
||||||
|
for tag in iter.tags() {
|
||||||
|
// Tag is link
|
||||||
|
if let Some(uri) = links.get(&tag) {
|
||||||
|
return open_link_in_current_tab(&uri.to_string(), &item_action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
secondary_button_controller.connect_pressed({
|
||||||
|
let links = links.clone();
|
||||||
|
let text_view = text_view.clone();
|
||||||
|
let link_context = link_context.clone();
|
||||||
|
move |_, _, window_x, window_y| {
|
||||||
|
let x = window_x as i32;
|
||||||
|
let y = window_y as i32;
|
||||||
|
// Detect tag match current coords hovered
|
||||||
|
let (buffer_x, buffer_y) =
|
||||||
|
text_view.window_to_buffer_coords(TextWindowType::Widget, x, y);
|
||||||
|
if let Some(iter) = text_view.iter_at_location(buffer_x, buffer_y) {
|
||||||
|
for tag in iter.tags() {
|
||||||
|
// Tag is link
|
||||||
|
if let Some(uri) = links.get(&tag) {
|
||||||
|
let request_str = uri.to_str();
|
||||||
|
let request_var = request_str.to_variant();
|
||||||
|
|
||||||
|
action_link_tab.set_state(&request_var);
|
||||||
|
action_link_copy.set_state(&request_var);
|
||||||
|
|
||||||
|
action_link_download.set_state(&request_var);
|
||||||
|
action_link_download.set_enabled(is_prefixable_link(&request_str));
|
||||||
|
|
||||||
|
action_link_source.set_state(&request_var);
|
||||||
|
action_link_source.set_enabled(is_prefixable_link(&request_str));
|
||||||
|
|
||||||
|
link_context
|
||||||
|
.set_pointing_to(Some(>k::gdk::Rectangle::new(x, y, 1, 1)));
|
||||||
|
link_context.popup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
middle_button_controller.connect_pressed({
|
||||||
|
let links = links.clone();
|
||||||
|
let text_view = text_view.clone();
|
||||||
|
let window_action = window_action.clone();
|
||||||
|
move |_, _, window_x, window_y| {
|
||||||
|
// Detect tag match current coords hovered
|
||||||
|
let (buffer_x, buffer_y) = text_view.window_to_buffer_coords(
|
||||||
|
TextWindowType::Widget,
|
||||||
|
window_x as i32,
|
||||||
|
window_y as i32,
|
||||||
|
);
|
||||||
|
if let Some(iter) = text_view.iter_at_location(buffer_x, buffer_y) {
|
||||||
|
for tag in iter.tags() {
|
||||||
|
// Tag is link
|
||||||
|
if let Some(uri) = links.get(&tag) {
|
||||||
|
return open_link_in_new_tab(&uri.to_string(), &window_action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}); // for a note: this action sensitive to focus out
|
||||||
|
|
||||||
|
motion_controller.connect_motion({
|
||||||
|
let text_view = text_view.clone();
|
||||||
|
let links = links.clone();
|
||||||
|
let hover = hover.clone();
|
||||||
|
move |_, window_x, window_y| {
|
||||||
|
// Detect tag match current coords hovered
|
||||||
|
let (buffer_x, buffer_y) = text_view.window_to_buffer_coords(
|
||||||
|
TextWindowType::Widget,
|
||||||
|
window_x as i32,
|
||||||
|
window_y as i32,
|
||||||
|
);
|
||||||
|
// Reset link colors to default
|
||||||
|
if let Some(tag) = hover.replace(None) {
|
||||||
|
tag.set_foreground_rgba(Some(&link_color.0));
|
||||||
|
}
|
||||||
|
// Apply hover effect
|
||||||
|
if let Some(iter) = text_view.iter_at_location(buffer_x, buffer_y) {
|
||||||
|
for tag in iter.tags() {
|
||||||
|
// Tag is link
|
||||||
|
if let Some(uri) = links.get(&tag) {
|
||||||
|
// Toggle color
|
||||||
|
tag.set_foreground_rgba(Some(&link_color.1));
|
||||||
|
// 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"));
|
||||||
|
// Redraw required to apply changes immediately
|
||||||
|
text_view.queue_draw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Restore defaults
|
||||||
|
gutter.set_uri(None);
|
||||||
|
text_view.set_cursor_from_name(Some("text"));
|
||||||
|
text_view.queue_draw();
|
||||||
|
}
|
||||||
|
}); // @TODO may be expensive for CPU, add timeout?
|
||||||
|
|
||||||
|
// Result
|
||||||
|
if is_code_enabled {
|
||||||
|
Ok(Self { text_view, title })
|
||||||
|
} else {
|
||||||
|
Err(Error::Markup(
|
||||||
|
"Invalid multiline markup! Gemtext format partially ignored.".to_string(),
|
||||||
|
Self { text_view, title },
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_internal_link(request: &str) -> bool {
|
||||||
|
// schemes
|
||||||
|
request.starts_with("gemini://")
|
||||||
|
|| request.starts_with("titan://")
|
||||||
|
|| request.starts_with("nex://")
|
||||||
|
|| request.starts_with("file://")
|
||||||
|
// prefix
|
||||||
|
|| request.starts_with("download:")
|
||||||
|
|| request.starts_with("source:")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_prefixable_link(request: &str) -> bool {
|
||||||
|
request.starts_with("gemini://")
|
||||||
|
|| request.starts_with("nex://")
|
||||||
|
|| request.starts_with("file://")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_link_in_external_app(request: &str) {
|
||||||
|
UriLauncher::new(request).launch(Window::NONE, Cancellable::NONE, |r| {
|
||||||
|
if let Err(e) = r {
|
||||||
|
println!("{e}") // @TODO use warn macro
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_link_in_current_tab(request: &str, item_action: &ItemAction) {
|
||||||
|
if is_internal_link(request) {
|
||||||
|
item_action.load.activate(Some(request), true, false)
|
||||||
|
} else {
|
||||||
|
open_link_in_external_app(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_link_in_new_tab(request: &str, window_action: &WindowAction) {
|
||||||
|
if is_internal_link(request) {
|
||||||
|
window_action.append.activate_stateful_once(
|
||||||
|
Position::After,
|
||||||
|
Some(request.into()),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
open_link_in_external_app(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn link_prefix(request: String, prefix: &str) -> String {
|
||||||
|
format!("{prefix}{}", request.trim_start_matches(prefix))
|
||||||
|
}
|
||||||
|
|
||||||
|
const LINK_PREFIX_DOWNLOAD: &str = "download:";
|
||||||
|
const LINK_PREFIX_SOURCE: &str = "source:";
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
mod rgba;
|
||||||
|
mod tag;
|
||||||
|
|
||||||
|
use tag::Tag;
|
||||||
|
|
||||||
|
use ansi_parser::{AnsiParser, AnsiSequence, Output};
|
||||||
|
use gtk::{TextTag, prelude::TextTagExt};
|
||||||
|
|
||||||
|
/// Apply ANSI/SGR format to new buffer
|
||||||
|
pub fn format(source_code: &str) -> Vec<(TextTag, String)> {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
let mut tag = Tag::new();
|
||||||
|
|
||||||
|
for ref entity in source_code.ansi_parse() {
|
||||||
|
if let Output::Escape(AnsiSequence::SetGraphicsMode(color)) = entity
|
||||||
|
&& color.len() > 1
|
||||||
|
{
|
||||||
|
if color[0] == 38 {
|
||||||
|
tag.text_tag
|
||||||
|
.set_foreground_rgba(rgba::default(*color.last().unwrap()).as_ref());
|
||||||
|
} else {
|
||||||
|
tag.text_tag
|
||||||
|
.set_background_rgba(rgba::default(*color.last().unwrap()).as_ref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Output::TextBlock(text) = entity {
|
||||||
|
buffer.push((tag.text_tag, text.to_string()));
|
||||||
|
tag = Tag::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,256 @@
|
||||||
|
use gtk::gdk::RGBA;
|
||||||
|
|
||||||
|
/// Default RGBa palette for ANSI terminal emulation
|
||||||
|
pub fn default(color: u8) -> Option<RGBA> {
|
||||||
|
match color {
|
||||||
|
7 => Some(RGBA::new(0.854, 0.854, 0.854, 1.0)),
|
||||||
|
8 => Some(RGBA::new(0.424, 0.424, 0.424, 1.0)),
|
||||||
|
10 => Some(RGBA::new(0.0, 1.0, 0.0, 1.0)),
|
||||||
|
11 => Some(RGBA::new(1.0, 1.0, 0.0, 1.0)),
|
||||||
|
12 => Some(RGBA::new(0.0, 0.0, 1.0, 1.0)),
|
||||||
|
13 => Some(RGBA::new(1.0, 0.0, 1.0, 1.0)),
|
||||||
|
14 => Some(RGBA::new(0.0, 1.0, 1.0, 1.0)),
|
||||||
|
15 => Some(RGBA::new(1.0, 1.0, 1.0, 1.0)),
|
||||||
|
16 => Some(RGBA::new(0.0, 0.0, 0.0, 1.0)),
|
||||||
|
17 => Some(RGBA::new(0.0, 0.020, 0.373, 1.0)),
|
||||||
|
18 => Some(RGBA::new(0.0, 0.031, 0.529, 1.0)),
|
||||||
|
19 => Some(RGBA::new(0.0, 0.0, 0.686, 1.0)),
|
||||||
|
20 => Some(RGBA::new(0.0, 0.0, 0.823, 1.0)),
|
||||||
|
21 => Some(RGBA::new(0.0, 0.0, 1.0, 1.0)),
|
||||||
|
22 => Some(RGBA::new(0.0, 0.373, 0.0, 1.0)),
|
||||||
|
23 => Some(RGBA::new(0.0, 0.373, 0.373, 1.0)),
|
||||||
|
24 => Some(RGBA::new(0.0, 0.373, 0.529, 1.0)),
|
||||||
|
25 => Some(RGBA::new(0.0, 0.373, 0.686, 1.0)),
|
||||||
|
26 => Some(RGBA::new(0.0, 0.373, 0.823, 1.0)),
|
||||||
|
27 => Some(RGBA::new(0.0, 0.373, 1.0, 1.0)),
|
||||||
|
28 => Some(RGBA::new(0.0, 0.533, 0.0, 1.0)),
|
||||||
|
29 => Some(RGBA::new(0.0, 0.533, 0.373, 1.0)),
|
||||||
|
30 => Some(RGBA::new(0.0, 0.533, 0.533, 1.0)),
|
||||||
|
31 => Some(RGBA::new(0.0, 0.533, 0.686, 1.0)),
|
||||||
|
32 => Some(RGBA::new(0.0, 0.533, 0.823, 1.0)),
|
||||||
|
33 => Some(RGBA::new(0.0, 0.533, 1.0, 1.0)),
|
||||||
|
34 => Some(RGBA::new(0.039, 0.686, 0.0, 1.0)),
|
||||||
|
35 => Some(RGBA::new(0.039, 0.686, 0.373, 1.0)),
|
||||||
|
36 => Some(RGBA::new(0.039, 0.686, 0.529, 1.0)),
|
||||||
|
37 => Some(RGBA::new(0.039, 0.686, 0.686, 1.0)),
|
||||||
|
38 => Some(RGBA::new(0.039, 0.686, 0.823, 1.0)),
|
||||||
|
39 => Some(RGBA::new(0.039, 0.686, 1.0, 1.0)),
|
||||||
|
40 => Some(RGBA::new(0.0, 0.843, 0.0, 1.0)),
|
||||||
|
41 => Some(RGBA::new(0.0, 0.843, 0.373, 1.0)),
|
||||||
|
42 => Some(RGBA::new(0.0, 0.843, 0.529, 1.0)),
|
||||||
|
43 => Some(RGBA::new(0.0, 0.843, 0.686, 1.0)),
|
||||||
|
44 => Some(RGBA::new(0.0, 0.843, 0.843, 1.0)),
|
||||||
|
45 => Some(RGBA::new(0.0, 0.843, 1.0, 1.0)),
|
||||||
|
46 => Some(RGBA::new(0.0, 1.0, 0.0, 1.0)),
|
||||||
|
47 => Some(RGBA::new(0.0, 1.0, 0.373, 1.0)),
|
||||||
|
48 => Some(RGBA::new(0.0, 1.0, 0.529, 1.0)),
|
||||||
|
49 => Some(RGBA::new(0.0, 1.0, 0.686, 1.0)),
|
||||||
|
50 => Some(RGBA::new(0.0, 1.0, 0.843, 1.0)),
|
||||||
|
51 => Some(RGBA::new(0.0, 1.0, 1.0, 1.0)),
|
||||||
|
52 => Some(RGBA::new(0.373, 0.0, 0.0, 1.0)),
|
||||||
|
53 => Some(RGBA::new(0.373, 0.0, 0.373, 1.0)),
|
||||||
|
54 => Some(RGBA::new(0.373, 0.0, 0.529, 1.0)),
|
||||||
|
55 => Some(RGBA::new(0.373, 0.0, 0.686, 1.0)),
|
||||||
|
56 => Some(RGBA::new(0.373, 0.0, 0.843, 1.0)),
|
||||||
|
57 => Some(RGBA::new(0.373, 0.0, 1.0, 1.0)),
|
||||||
|
58 => Some(RGBA::new(0.373, 0.373, 0.0, 1.0)),
|
||||||
|
59 => Some(RGBA::new(0.373, 0.373, 0.373, 1.0)),
|
||||||
|
60 => Some(RGBA::new(0.373, 0.373, 0.529, 1.0)),
|
||||||
|
61 => Some(RGBA::new(0.373, 0.373, 0.686, 1.0)),
|
||||||
|
62 => Some(RGBA::new(0.373, 0.373, 0.843, 1.0)),
|
||||||
|
63 => Some(RGBA::new(0.373, 0.373, 1.0, 1.0)),
|
||||||
|
64 => Some(RGBA::new(0.373, 0.529, 0.0, 1.0)),
|
||||||
|
65 => Some(RGBA::new(0.373, 0.529, 0.373, 1.0)),
|
||||||
|
66 => Some(RGBA::new(0.373, 0.529, 0.529, 1.0)),
|
||||||
|
67 => Some(RGBA::new(0.373, 0.529, 0.686, 1.0)),
|
||||||
|
68 => Some(RGBA::new(0.373, 0.529, 0.843, 1.0)),
|
||||||
|
69 => Some(RGBA::new(0.373, 0.529, 1.0, 1.0)),
|
||||||
|
70 => Some(RGBA::new(0.373, 0.686, 0.0, 1.0)),
|
||||||
|
71 => Some(RGBA::new(0.373, 0.686, 0.373, 1.0)),
|
||||||
|
72 => Some(RGBA::new(0.373, 0.686, 0.529, 1.0)),
|
||||||
|
73 => Some(RGBA::new(0.373, 0.686, 0.686, 1.0)),
|
||||||
|
74 => Some(RGBA::new(0.373, 0.686, 0.843, 1.0)),
|
||||||
|
75 => Some(RGBA::new(0.373, 0.686, 1.0, 1.0)),
|
||||||
|
76 => Some(RGBA::new(0.373, 0.843, 0.0, 1.0)),
|
||||||
|
77 => Some(RGBA::new(0.373, 0.843, 0.373, 1.0)),
|
||||||
|
78 => Some(RGBA::new(0.373, 0.843, 0.529, 1.0)),
|
||||||
|
79 => Some(RGBA::new(0.373, 0.843, 0.686, 1.0)),
|
||||||
|
80 => Some(RGBA::new(0.373, 0.843, 0.843, 1.0)),
|
||||||
|
81 => Some(RGBA::new(0.373, 0.843, 1.0, 1.0)),
|
||||||
|
82 => Some(RGBA::new(0.373, 1.0, 0.0, 1.0)),
|
||||||
|
83 => Some(RGBA::new(0.373, 1.0, 0.373, 1.0)),
|
||||||
|
84 => Some(RGBA::new(0.373, 1.0, 0.529, 1.0)),
|
||||||
|
85 => Some(RGBA::new(0.373, 1.0, 0.686, 1.0)),
|
||||||
|
86 => Some(RGBA::new(0.373, 1.0, 0.843, 1.0)),
|
||||||
|
87 => Some(RGBA::new(0.373, 1.0, 1.0, 1.0)),
|
||||||
|
88 => Some(RGBA::new(0.529, 0.0, 0.0, 1.0)),
|
||||||
|
89 => Some(RGBA::new(0.529, 0.0, 0.373, 1.0)),
|
||||||
|
90 => Some(RGBA::new(0.529, 0.0, 0.529, 1.0)),
|
||||||
|
91 => Some(RGBA::new(0.529, 0.0, 0.686, 1.0)),
|
||||||
|
92 => Some(RGBA::new(0.529, 0.0, 0.843, 1.0)),
|
||||||
|
93 => Some(RGBA::new(0.529, 0.0, 1.0, 1.0)),
|
||||||
|
94 => Some(RGBA::new(0.529, 0.373, 0.0, 1.0)),
|
||||||
|
95 => Some(RGBA::new(0.529, 0.373, 0.373, 1.0)),
|
||||||
|
96 => Some(RGBA::new(0.529, 0.373, 0.529, 1.0)),
|
||||||
|
97 => Some(RGBA::new(0.529, 0.373, 0.686, 1.0)),
|
||||||
|
98 => Some(RGBA::new(0.529, 0.373, 0.843, 1.0)),
|
||||||
|
99 => Some(RGBA::new(0.529, 0.373, 1.0, 1.0)),
|
||||||
|
100 => Some(RGBA::new(0.529, 0.529, 0.0, 1.0)),
|
||||||
|
101 => Some(RGBA::new(0.529, 0.529, 0.373, 1.0)),
|
||||||
|
102 => Some(RGBA::new(0.529, 0.529, 0.529, 1.0)),
|
||||||
|
103 => Some(RGBA::new(0.529, 0.529, 0.686, 1.0)),
|
||||||
|
104 => Some(RGBA::new(0.529, 0.529, 0.843, 1.0)),
|
||||||
|
105 => Some(RGBA::new(0.529, 0.529, 1.0, 1.0)),
|
||||||
|
106 => Some(RGBA::new(0.533, 0.686, 0.0, 1.0)),
|
||||||
|
107 => Some(RGBA::new(0.533, 0.686, 0.373, 1.0)),
|
||||||
|
108 => Some(RGBA::new(0.533, 0.686, 0.529, 1.0)),
|
||||||
|
109 => Some(RGBA::new(0.533, 0.686, 0.686, 1.0)),
|
||||||
|
110 => Some(RGBA::new(0.533, 0.686, 0.843, 1.0)),
|
||||||
|
111 => Some(RGBA::new(0.533, 0.686, 1.0, 1.0)),
|
||||||
|
112 => Some(RGBA::new(0.533, 0.843, 0.0, 1.0)),
|
||||||
|
113 => Some(RGBA::new(0.533, 0.843, 0.373, 1.0)),
|
||||||
|
114 => Some(RGBA::new(0.533, 0.843, 0.529, 1.0)),
|
||||||
|
115 => Some(RGBA::new(0.533, 0.843, 0.686, 1.0)),
|
||||||
|
116 => Some(RGBA::new(0.533, 0.843, 0.843, 1.0)),
|
||||||
|
117 => Some(RGBA::new(0.533, 0.843, 1.0, 1.0)),
|
||||||
|
118 => Some(RGBA::new(0.533, 1.0, 0.0, 1.0)),
|
||||||
|
119 => Some(RGBA::new(0.533, 1.0, 0.373, 1.0)),
|
||||||
|
120 => Some(RGBA::new(0.533, 1.0, 0.529, 1.0)),
|
||||||
|
121 => Some(RGBA::new(0.533, 1.0, 0.686, 1.0)),
|
||||||
|
122 => Some(RGBA::new(0.533, 1.0, 0.843, 1.0)),
|
||||||
|
123 => Some(RGBA::new(0.533, 1.0, 1.0, 1.0)),
|
||||||
|
124 => Some(RGBA::new(0.686, 0.0, 0.0, 1.0)),
|
||||||
|
125 => Some(RGBA::new(0.686, 0.0, 0.373, 1.0)),
|
||||||
|
126 => Some(RGBA::new(0.686, 0.0, 0.529, 1.0)),
|
||||||
|
127 => Some(RGBA::new(0.686, 0.0, 0.686, 1.0)),
|
||||||
|
128 => Some(RGBA::new(0.686, 0.0, 0.843, 1.0)),
|
||||||
|
129 => Some(RGBA::new(0.686, 0.0, 1.0, 1.0)),
|
||||||
|
130 => Some(RGBA::new(0.686, 0.373, 0.0, 1.0)),
|
||||||
|
131 => Some(RGBA::new(0.686, 0.373, 0.373, 1.0)),
|
||||||
|
132 => Some(RGBA::new(0.686, 0.373, 0.529, 1.0)),
|
||||||
|
133 => Some(RGBA::new(0.686, 0.373, 0.686, 1.0)),
|
||||||
|
134 => Some(RGBA::new(0.686, 0.373, 0.843, 1.0)),
|
||||||
|
135 => Some(RGBA::new(0.686, 0.373, 1.0, 1.0)),
|
||||||
|
136 => Some(RGBA::new(0.686, 0.529, 0.0, 1.0)),
|
||||||
|
137 => Some(RGBA::new(0.686, 0.529, 0.373, 1.0)),
|
||||||
|
138 => Some(RGBA::new(0.686, 0.529, 0.529, 1.0)),
|
||||||
|
139 => Some(RGBA::new(0.686, 0.529, 0.686, 1.0)),
|
||||||
|
140 => Some(RGBA::new(0.686, 0.529, 0.843, 1.0)),
|
||||||
|
141 => Some(RGBA::new(0.686, 0.529, 1.0, 1.0)),
|
||||||
|
142 => Some(RGBA::new(0.686, 0.686, 0.0, 1.0)),
|
||||||
|
143 => Some(RGBA::new(0.686, 0.686, 0.373, 1.0)),
|
||||||
|
144 => Some(RGBA::new(0.686, 0.686, 0.529, 1.0)),
|
||||||
|
145 => Some(RGBA::new(0.686, 0.686, 0.686, 1.0)),
|
||||||
|
146 => Some(RGBA::new(0.686, 0.686, 0.843, 1.0)),
|
||||||
|
147 => Some(RGBA::new(0.686, 0.686, 1.0, 1.0)),
|
||||||
|
148 => Some(RGBA::new(0.686, 0.843, 0.0, 1.0)),
|
||||||
|
149 => Some(RGBA::new(0.686, 0.843, 0.373, 1.0)),
|
||||||
|
150 => Some(RGBA::new(0.686, 0.843, 0.529, 1.0)),
|
||||||
|
151 => Some(RGBA::new(0.686, 0.843, 0.686, 1.0)),
|
||||||
|
152 => Some(RGBA::new(0.686, 0.843, 0.843, 1.0)),
|
||||||
|
153 => Some(RGBA::new(0.686, 0.843, 1.0, 1.0)),
|
||||||
|
154 => Some(RGBA::new(0.686, 1.0, 0.0, 1.0)),
|
||||||
|
155 => Some(RGBA::new(0.686, 1.0, 0.373, 1.0)),
|
||||||
|
156 => Some(RGBA::new(0.686, 1.0, 0.529, 1.0)),
|
||||||
|
157 => Some(RGBA::new(0.686, 1.0, 0.686, 1.0)),
|
||||||
|
158 => Some(RGBA::new(0.686, 1.0, 0.843, 1.0)),
|
||||||
|
159 => Some(RGBA::new(0.686, 1.0, 1.0, 1.0)),
|
||||||
|
160 => Some(RGBA::new(0.847, 0.0, 0.0, 1.0)),
|
||||||
|
161 => Some(RGBA::new(0.847, 0.0, 0.373, 1.0)),
|
||||||
|
162 => Some(RGBA::new(0.847, 0.0, 0.529, 1.0)),
|
||||||
|
163 => Some(RGBA::new(0.847, 0.0, 0.686, 1.0)),
|
||||||
|
164 => Some(RGBA::new(0.847, 0.0, 0.843, 1.0)),
|
||||||
|
165 => Some(RGBA::new(0.847, 0.0, 1.0, 1.0)),
|
||||||
|
166 => Some(RGBA::new(0.847, 0.373, 0.0, 1.0)),
|
||||||
|
167 => Some(RGBA::new(0.847, 0.373, 0.373, 1.0)),
|
||||||
|
168 => Some(RGBA::new(0.847, 0.373, 0.529, 1.0)),
|
||||||
|
169 => Some(RGBA::new(0.847, 0.373, 0.686, 1.0)),
|
||||||
|
170 => Some(RGBA::new(0.847, 0.373, 0.843, 1.0)),
|
||||||
|
171 => Some(RGBA::new(0.847, 0.373, 1.0, 1.0)),
|
||||||
|
172 => Some(RGBA::new(0.847, 0.529, 0.0, 1.0)),
|
||||||
|
173 => Some(RGBA::new(0.847, 0.529, 0.373, 1.0)),
|
||||||
|
174 => Some(RGBA::new(0.847, 0.529, 0.529, 1.0)),
|
||||||
|
175 => Some(RGBA::new(0.847, 0.529, 0.686, 1.0)),
|
||||||
|
176 => Some(RGBA::new(0.847, 0.529, 0.843, 1.0)),
|
||||||
|
177 => Some(RGBA::new(0.847, 0.529, 1.0, 1.0)),
|
||||||
|
178 => Some(RGBA::new(0.847, 0.686, 0.0, 1.0)),
|
||||||
|
179 => Some(RGBA::new(0.847, 0.686, 0.373, 1.0)),
|
||||||
|
180 => Some(RGBA::new(0.847, 0.686, 0.529, 1.0)),
|
||||||
|
181 => Some(RGBA::new(0.847, 0.686, 0.686, 1.0)),
|
||||||
|
182 => Some(RGBA::new(0.847, 0.686, 0.843, 1.0)),
|
||||||
|
183 => Some(RGBA::new(0.847, 0.686, 1.0, 1.0)),
|
||||||
|
184 => Some(RGBA::new(0.847, 0.843, 0.0, 1.0)),
|
||||||
|
185 => Some(RGBA::new(0.847, 0.843, 0.373, 1.0)),
|
||||||
|
186 => Some(RGBA::new(0.847, 0.843, 0.529, 1.0)),
|
||||||
|
187 => Some(RGBA::new(0.847, 0.843, 0.686, 1.0)),
|
||||||
|
188 => Some(RGBA::new(0.847, 0.843, 0.843, 1.0)),
|
||||||
|
189 => Some(RGBA::new(0.847, 0.843, 1.0, 1.0)),
|
||||||
|
190 => Some(RGBA::new(0.847, 1.0, 0.0, 1.0)),
|
||||||
|
191 => Some(RGBA::new(0.847, 1.0, 0.373, 1.0)),
|
||||||
|
192 => Some(RGBA::new(0.847, 1.0, 0.529, 1.0)),
|
||||||
|
193 => Some(RGBA::new(0.847, 1.0, 0.686, 1.0)),
|
||||||
|
194 => Some(RGBA::new(0.847, 1.0, 0.843, 1.0)),
|
||||||
|
195 => Some(RGBA::new(0.847, 1.0, 1.0, 1.0)),
|
||||||
|
196 => Some(RGBA::new(1.0, 0.0, 0.0, 1.0)),
|
||||||
|
197 => Some(RGBA::new(1.0, 0.0, 0.373, 1.0)),
|
||||||
|
198 => Some(RGBA::new(1.0, 0.0, 0.529, 1.0)),
|
||||||
|
199 => Some(RGBA::new(1.0, 0.0, 0.686, 1.0)),
|
||||||
|
200 => Some(RGBA::new(1.0, 0.0, 0.843, 1.0)),
|
||||||
|
201 => Some(RGBA::new(1.0, 0.0, 1.0, 1.0)),
|
||||||
|
202 => Some(RGBA::new(1.0, 0.373, 0.0, 1.0)),
|
||||||
|
203 => Some(RGBA::new(1.0, 0.373, 0.373, 1.0)),
|
||||||
|
204 => Some(RGBA::new(1.0, 0.373, 0.529, 1.0)),
|
||||||
|
205 => Some(RGBA::new(1.0, 0.373, 0.686, 1.0)),
|
||||||
|
206 => Some(RGBA::new(1.0, 0.373, 0.843, 1.0)),
|
||||||
|
207 => Some(RGBA::new(1.0, 0.373, 1.0, 1.0)),
|
||||||
|
208 => Some(RGBA::new(1.0, 0.529, 0.0, 1.0)),
|
||||||
|
209 => Some(RGBA::new(1.0, 0.529, 0.373, 1.0)),
|
||||||
|
210 => Some(RGBA::new(1.0, 0.529, 0.529, 1.0)),
|
||||||
|
211 => Some(RGBA::new(1.0, 0.529, 0.686, 1.0)),
|
||||||
|
212 => Some(RGBA::new(1.0, 0.529, 0.843, 1.0)),
|
||||||
|
213 => Some(RGBA::new(1.0, 0.529, 1.0, 1.0)),
|
||||||
|
214 => Some(RGBA::new(1.0, 0.686, 0.0, 1.0)),
|
||||||
|
215 => Some(RGBA::new(1.0, 0.686, 0.373, 1.0)),
|
||||||
|
216 => Some(RGBA::new(1.0, 0.686, 0.529, 1.0)),
|
||||||
|
217 => Some(RGBA::new(1.0, 0.686, 0.686, 1.0)),
|
||||||
|
218 => Some(RGBA::new(1.0, 0.686, 0.843, 1.0)),
|
||||||
|
219 => Some(RGBA::new(1.0, 0.686, 1.0, 1.0)),
|
||||||
|
220 => Some(RGBA::new(1.0, 0.843, 0.0, 1.0)),
|
||||||
|
221 => Some(RGBA::new(1.0, 0.843, 0.373, 1.0)),
|
||||||
|
222 => Some(RGBA::new(1.0, 0.843, 0.529, 1.0)),
|
||||||
|
223 => Some(RGBA::new(1.0, 0.843, 0.686, 1.0)),
|
||||||
|
224 => Some(RGBA::new(1.0, 0.843, 0.843, 1.0)),
|
||||||
|
225 => Some(RGBA::new(1.0, 0.843, 1.0, 1.0)),
|
||||||
|
226 => Some(RGBA::new(1.0, 1.0, 0.0, 1.0)),
|
||||||
|
227 => Some(RGBA::new(1.0, 1.0, 0.373, 1.0)),
|
||||||
|
228 => Some(RGBA::new(1.0, 1.0, 0.529, 1.0)),
|
||||||
|
229 => Some(RGBA::new(1.0, 1.0, 0.686, 1.0)),
|
||||||
|
230 => Some(RGBA::new(1.0, 1.0, 0.843, 1.0)),
|
||||||
|
231 => Some(RGBA::new(1.0, 1.0, 1.0, 1.0)),
|
||||||
|
232 => Some(RGBA::new(0.031, 0.031, 0.031, 1.0)),
|
||||||
|
233 => Some(RGBA::new(0.071, 0.071, 0.071, 1.0)),
|
||||||
|
234 => Some(RGBA::new(0.110, 0.110, 0.110, 1.0)),
|
||||||
|
235 => Some(RGBA::new(0.149, 0.149, 0.149, 1.0)),
|
||||||
|
236 => Some(RGBA::new(0.188, 0.188, 0.188, 1.0)),
|
||||||
|
237 => Some(RGBA::new(0.227, 0.227, 0.227, 1.0)),
|
||||||
|
238 => Some(RGBA::new(0.267, 0.267, 0.267, 1.0)),
|
||||||
|
239 => Some(RGBA::new(0.306, 0.306, 0.306, 1.0)),
|
||||||
|
240 => Some(RGBA::new(0.345, 0.345, 0.345, 1.0)),
|
||||||
|
241 => Some(RGBA::new(0.384, 0.384, 0.384, 1.0)),
|
||||||
|
242 => Some(RGBA::new(0.424, 0.424, 0.424, 1.0)),
|
||||||
|
243 => Some(RGBA::new(0.462, 0.462, 0.462, 1.0)),
|
||||||
|
244 => Some(RGBA::new(0.502, 0.502, 0.502, 1.0)),
|
||||||
|
245 => Some(RGBA::new(0.541, 0.541, 0.541, 1.0)),
|
||||||
|
246 => Some(RGBA::new(0.580, 0.580, 0.580, 1.0)),
|
||||||
|
247 => Some(RGBA::new(0.620, 0.620, 0.620, 1.0)),
|
||||||
|
248 => Some(RGBA::new(0.659, 0.659, 0.659, 1.0)),
|
||||||
|
249 => Some(RGBA::new(0.694, 0.694, 0.694, 1.0)),
|
||||||
|
250 => Some(RGBA::new(0.733, 0.733, 0.733, 1.0)),
|
||||||
|
251 => Some(RGBA::new(0.777, 0.777, 0.777, 1.0)),
|
||||||
|
252 => Some(RGBA::new(0.816, 0.816, 0.816, 1.0)),
|
||||||
|
253 => Some(RGBA::new(0.855, 0.855, 0.855, 1.0)),
|
||||||
|
254 => Some(RGBA::new(0.890, 0.890, 0.890, 1.0)),
|
||||||
|
255 => Some(RGBA::new(0.933, 0.933, 0.933, 1.0)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
use gtk::{TextTag, WrapMode};
|
||||||
|
|
||||||
|
/// Default [TextTag](https://docs.gtk.org/gtk4/class.TextTag.html) preset
|
||||||
|
/// for ANSI buffer
|
||||||
|
pub struct Tag {
|
||||||
|
pub text_tag: TextTag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Tag {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tag {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// Create new `Self`
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
text_tag: TextTag::builder()
|
||||||
|
.family("monospace") // @TODO
|
||||||
|
.left_margin(28)
|
||||||
|
.scale(0.81) // * the rounded `0.8` value crops text for some reason @TODO
|
||||||
|
.wrap_mode(WrapMode::None)
|
||||||
|
.build(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub enum Error {
|
||||||
|
Markup(String, super::Markdown),
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
use gtk::{
|
||||||
|
Align, Label, TextView, TextWindowType,
|
||||||
|
glib::{Uri, timeout_add_local_once},
|
||||||
|
pango::EllipsizeMode,
|
||||||
|
prelude::{TextViewExt, WidgetExt},
|
||||||
|
};
|
||||||
|
use std::{cell::Cell, rc::Rc, time::Duration};
|
||||||
|
|
||||||
|
pub struct Gutter {
|
||||||
|
pub label: Label,
|
||||||
|
is_active: Rc<Cell<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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"])
|
||||||
|
.ellipsize(EllipsizeMode::Middle)
|
||||||
|
.halign(Align::Start)
|
||||||
|
.margin_bottom(MARGIN_Y)
|
||||||
|
.margin_end(MARGIN_X)
|
||||||
|
.margin_start(MARGIN_X)
|
||||||
|
.margin_top(MARGIN_Y)
|
||||||
|
.visible(false)
|
||||||
|
.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(250), {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
use gtk::{IconLookupFlags, IconPaintable, IconTheme, TextDirection, gdk::Display};
|
||||||
|
|
||||||
|
const SIZE: i32 = 16;
|
||||||
|
|
||||||
|
/// Indication icons asset (for tag blocks decoration)
|
||||||
|
pub struct Icon {
|
||||||
|
pub quote: IconPaintable,
|
||||||
|
// @TODO other tags..
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Icon {
|
||||||
|
pub fn new() -> Option<Self> {
|
||||||
|
Display::default().map(|display| {
|
||||||
|
let theme = IconTheme::for_display(&display);
|
||||||
|
Self {
|
||||||
|
quote: icon(&theme, "mail-forward-symbolic"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn icon(theme: &IconTheme, name: &str) -> IconPaintable {
|
||||||
|
theme.lookup_icon(
|
||||||
|
name,
|
||||||
|
&[], // @TODO
|
||||||
|
SIZE,
|
||||||
|
SIZE,
|
||||||
|
TextDirection::None,
|
||||||
|
IconLookupFlags::FORCE_SYMBOLIC,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod code;
|
||||||
|
pub mod header;
|
||||||
|
pub mod link;
|
||||||
|
pub mod list;
|
||||||
|
pub mod quote;
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
pub mod error;
|
||||||
|
mod tag;
|
||||||
|
|
||||||
|
pub use error::Error;
|
||||||
|
use tag::Tag;
|
||||||
|
|
||||||
|
use adw::StyleManager;
|
||||||
|
use gtk::{
|
||||||
|
TextTag,
|
||||||
|
gdk::RGBA,
|
||||||
|
pango::{Style, Underline},
|
||||||
|
prelude::TextTagExt,
|
||||||
|
};
|
||||||
|
use syntect::{
|
||||||
|
easy::HighlightLines,
|
||||||
|
highlighting::{Color, FontStyle, ThemeSet},
|
||||||
|
parsing::{SyntaxReference, SyntaxSet},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Default theme
|
||||||
|
@TODO make optional
|
||||||
|
base16-ocean.dark
|
||||||
|
base16-eighties.dark
|
||||||
|
base16-mocha.dark
|
||||||
|
base16-ocean.light
|
||||||
|
InspiredGitHub
|
||||||
|
Solarized (dark)
|
||||||
|
Solarized (light)
|
||||||
|
*/
|
||||||
|
pub const DEFAULT_THEME_DARK: &str = "base16-eighties.dark";
|
||||||
|
pub const DEFAULT_THEME_LIGHT: &str = "InspiredGitHub";
|
||||||
|
|
||||||
|
pub struct Syntax {
|
||||||
|
syntax_set: SyntaxSet,
|
||||||
|
theme_set: ThemeSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Syntax {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Syntax {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// Create new `Self`
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
syntax_set: SyntaxSet::load_defaults_newlines(),
|
||||||
|
theme_set: ThemeSet::load_defaults(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
|
||||||
|
/// Apply `Syntect` highlight to new buffer returned,
|
||||||
|
/// according to given `alt` and `source_code` content
|
||||||
|
pub fn highlight(
|
||||||
|
&self,
|
||||||
|
source_code: &str,
|
||||||
|
alt: Option<&String>,
|
||||||
|
) -> Result<Vec<(TextTag, String)>, Error> {
|
||||||
|
if let Some(value) = alt {
|
||||||
|
if let Some(reference) = self.syntax_set.find_syntax_by_name(value) {
|
||||||
|
return self.buffer(source_code, reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(reference) = self.syntax_set.find_syntax_by_token(value) {
|
||||||
|
return self.buffer(source_code, reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(reference) = self.syntax_set.find_syntax_by_path(value) {
|
||||||
|
return self.buffer(source_code, reference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(reference) = self.syntax_set.find_syntax_by_first_line(source_code) {
|
||||||
|
return self.buffer(source_code, reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(Error::Parse)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffer(
|
||||||
|
&self,
|
||||||
|
source: &str,
|
||||||
|
syntax_reference: &SyntaxReference,
|
||||||
|
) -> Result<Vec<(TextTag, String)>, Error> {
|
||||||
|
// Init new line buffer
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
// Apply syntect decorator
|
||||||
|
let mut ranges = HighlightLines::new(
|
||||||
|
syntax_reference,
|
||||||
|
&self.theme_set.themes[if StyleManager::default().is_dark() {
|
||||||
|
DEFAULT_THEME_DARK
|
||||||
|
} else {
|
||||||
|
DEFAULT_THEME_LIGHT
|
||||||
|
}], // @TODO apply on env change
|
||||||
|
);
|
||||||
|
|
||||||
|
match ranges.highlight_line(source, &self.syntax_set) {
|
||||||
|
Ok(result) => {
|
||||||
|
// Build tags
|
||||||
|
for (style, entity) in result {
|
||||||
|
// Create new tag from default preset
|
||||||
|
let tag = Tag::new();
|
||||||
|
|
||||||
|
// Tuneup using syntect conversion
|
||||||
|
// tag.set_background_rgba(Some(&color_to_rgba(style.background)));
|
||||||
|
tag.text_tag
|
||||||
|
.set_foreground_rgba(Some(&color_to_rgba(style.foreground)));
|
||||||
|
tag.text_tag
|
||||||
|
.set_style(font_style_to_style(style.font_style));
|
||||||
|
tag.text_tag
|
||||||
|
.set_underline(font_style_to_underline(style.font_style));
|
||||||
|
|
||||||
|
// Append
|
||||||
|
buffer.push((tag.text_tag, entity.to_string()));
|
||||||
|
}
|
||||||
|
Ok(buffer)
|
||||||
|
}
|
||||||
|
Err(e) => Err(Error::Syntect(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tools
|
||||||
|
|
||||||
|
fn color_to_rgba(color: Color) -> RGBA {
|
||||||
|
RGBA::new(
|
||||||
|
color.r as f32 / 255.0,
|
||||||
|
color.g as f32 / 255.0,
|
||||||
|
color.b as f32 / 255.0,
|
||||||
|
color.a as f32 / 255.0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn font_style_to_style(font_style: FontStyle) -> Style {
|
||||||
|
match font_style {
|
||||||
|
FontStyle::ITALIC => Style::Italic,
|
||||||
|
_ => Style::Normal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn font_style_to_underline(font_style: FontStyle) -> Underline {
|
||||||
|
match font_style {
|
||||||
|
FontStyle::UNDERLINE => Underline::Single,
|
||||||
|
_ => Underline::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
use std::fmt::{Display, Formatter, Result};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Parse,
|
||||||
|
Syntect(syntect::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
match self {
|
||||||
|
Self::Parse => write!(f, "Parse error"),
|
||||||
|
Self::Syntect(e) => {
|
||||||
|
write!(f, "Syntect error: {e}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
use gtk::{TextTag, WrapMode};
|
||||||
|
|
||||||
|
/// Default [TextTag](https://docs.gtk.org/gtk4/class.TextTag.html) preset
|
||||||
|
/// for syntax highlight buffer
|
||||||
|
pub struct Tag {
|
||||||
|
pub text_tag: TextTag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Tag {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tag {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// Create new `Self`
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
text_tag: TextTag::builder()
|
||||||
|
.family("monospace") // @TODO
|
||||||
|
.left_margin(28)
|
||||||
|
.scale(0.81) // * the rounded `0.8` value crops text for some reason @TODO
|
||||||
|
.wrap_mode(WrapMode::None)
|
||||||
|
.build(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
mod header;
|
||||||
|
mod list;
|
||||||
|
mod plain;
|
||||||
|
mod quote;
|
||||||
|
mod title;
|
||||||
|
|
||||||
|
use gtk::{TextTag, TextTagTable};
|
||||||
|
use header::Header;
|
||||||
|
use list::List;
|
||||||
|
use plain::Plain;
|
||||||
|
use quote::Quote;
|
||||||
|
use title::Title;
|
||||||
|
|
||||||
|
pub struct Tag {
|
||||||
|
pub text_tag_table: TextTagTable,
|
||||||
|
// Tags
|
||||||
|
pub h1: TextTag,
|
||||||
|
pub h2: TextTag,
|
||||||
|
pub h3: TextTag,
|
||||||
|
pub list: TextTag,
|
||||||
|
pub quote: TextTag,
|
||||||
|
pub title: TextTag,
|
||||||
|
pub plain: TextTag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Tag {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tag {
|
||||||
|
// Construct
|
||||||
|
pub fn new() -> Self {
|
||||||
|
// Init components
|
||||||
|
let h1 = TextTag::h1();
|
||||||
|
let h2 = TextTag::h2();
|
||||||
|
let h3 = TextTag::h3();
|
||||||
|
let list = TextTag::list();
|
||||||
|
let quote = TextTag::quote();
|
||||||
|
let title = TextTag::title();
|
||||||
|
let plain = TextTag::plain();
|
||||||
|
|
||||||
|
// Init tag table
|
||||||
|
let text_tag_table = TextTagTable::new();
|
||||||
|
|
||||||
|
text_tag_table.add(&h1);
|
||||||
|
text_tag_table.add(&h2);
|
||||||
|
text_tag_table.add(&h3);
|
||||||
|
text_tag_table.add(&title);
|
||||||
|
text_tag_table.add(&list);
|
||||||
|
text_tag_table.add("e);
|
||||||
|
text_tag_table.add(&plain);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
text_tag_table,
|
||||||
|
// Tags
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
list,
|
||||||
|
quote,
|
||||||
|
title,
|
||||||
|
plain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
use gtk::{TextTag, WrapMode};
|
||||||
|
|
||||||
|
pub trait Header {
|
||||||
|
fn h1() -> Self;
|
||||||
|
fn h2() -> Self;
|
||||||
|
fn h3() -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Header for TextTag {
|
||||||
|
fn h1() -> Self {
|
||||||
|
TextTag::builder()
|
||||||
|
.foreground("#2190a4") // @TODO optional
|
||||||
|
.scale(1.6)
|
||||||
|
.sentence(true)
|
||||||
|
.weight(500)
|
||||||
|
.wrap_mode(WrapMode::Word)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
fn h2() -> Self {
|
||||||
|
TextTag::builder()
|
||||||
|
.foreground("#d56199") // @TODO optional
|
||||||
|
.scale(1.4)
|
||||||
|
.sentence(true)
|
||||||
|
.weight(400)
|
||||||
|
.wrap_mode(WrapMode::Word)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
fn h3() -> Self {
|
||||||
|
TextTag::builder()
|
||||||
|
.foreground("#c88800") // @TODO optional
|
||||||
|
.scale(1.2)
|
||||||
|
.sentence(true)
|
||||||
|
.weight(400)
|
||||||
|
.wrap_mode(WrapMode::Word)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
use gtk::{TextTag, WrapMode};
|
||||||
|
|
||||||
|
pub trait List {
|
||||||
|
fn list() -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl List for TextTag {
|
||||||
|
fn list() -> Self {
|
||||||
|
TextTag::builder()
|
||||||
|
.left_margin(28)
|
||||||
|
.pixels_above_lines(4)
|
||||||
|
.pixels_below_lines(4)
|
||||||
|
.wrap_mode(WrapMode::Word)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
use gtk::{TextTag, WrapMode};
|
||||||
|
|
||||||
|
pub trait Plain {
|
||||||
|
fn plain() -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plain for TextTag {
|
||||||
|
fn plain() -> Self {
|
||||||
|
TextTag::builder().wrap_mode(WrapMode::Word).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
use gtk::{TextTag, WrapMode};
|
||||||
|
|
||||||
|
pub trait Quote {
|
||||||
|
fn quote() -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Quote for TextTag {
|
||||||
|
fn quote() -> Self {
|
||||||
|
TextTag::builder()
|
||||||
|
.left_margin(28)
|
||||||
|
.wrap_mode(WrapMode::Word)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
use gtk::{TextTag, WrapMode};
|
||||||
|
|
||||||
|
pub trait Title {
|
||||||
|
fn title() -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Title for TextTag {
|
||||||
|
fn title() -> Self {
|
||||||
|
TextTag::builder()
|
||||||
|
.pixels_above_lines(4)
|
||||||
|
.pixels_below_lines(8)
|
||||||
|
.weight(500)
|
||||||
|
.wrap_mode(WrapMode::None)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue