mirror of
https://github.com/YGGverse/Yoda.git
synced 2026-04-01 17:15:28 +00:00
normalize tab items component
This commit is contained in:
parent
47e2bc4617
commit
65502c247d
29 changed files with 427 additions and 117 deletions
50
src/app/browser/window/tab/item/page/content/text.rs
Normal file
50
src/app/browser/window/tab/item/page/content/text.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
mod gemini;
|
||||
|
||||
use gemini::Gemini;
|
||||
|
||||
use gtk::{
|
||||
gio::SimpleAction,
|
||||
glib::{GString, Uri},
|
||||
ScrolledWindow,
|
||||
};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Meta {
|
||||
title: Option<GString>,
|
||||
}
|
||||
|
||||
pub struct Text {
|
||||
meta: Meta,
|
||||
widget: ScrolledWindow,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
// Construct
|
||||
pub fn gemini(gemtext: &str, base: &Uri, action_page_open: Arc<SimpleAction>) -> Self {
|
||||
// Init components
|
||||
let gemini = Gemini::new(gemtext, base, action_page_open);
|
||||
|
||||
// Init meta
|
||||
let meta = Meta {
|
||||
title: gemini.reader_title().clone(),
|
||||
};
|
||||
|
||||
// Init widget
|
||||
let widget = ScrolledWindow::builder().build();
|
||||
|
||||
widget.set_child(Some(gemini.widget()));
|
||||
|
||||
// Result
|
||||
Self { meta, widget }
|
||||
}
|
||||
|
||||
// Getters
|
||||
pub fn meta_title(&self) -> &Option<GString> {
|
||||
&self.meta.title
|
||||
}
|
||||
|
||||
pub fn widget(&self) -> &ScrolledWindow {
|
||||
&self.widget
|
||||
}
|
||||
}
|
||||
41
src/app/browser/window/tab/item/page/content/text/gemini.rs
Normal file
41
src/app/browser/window/tab/item/page/content/text/gemini.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
mod reader;
|
||||
|
||||
use reader::Reader;
|
||||
|
||||
use gtk::{
|
||||
gio::SimpleAction,
|
||||
glib::{GString, Uri},
|
||||
Viewport,
|
||||
};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Gemini {
|
||||
reader: Reader,
|
||||
widget: Viewport,
|
||||
}
|
||||
|
||||
impl Gemini {
|
||||
// Construct
|
||||
pub fn new(gemtext: &str, base: &Uri, action_page_open: Arc<SimpleAction>) -> Self {
|
||||
// Init components
|
||||
let reader = Reader::new(gemtext, base, action_page_open);
|
||||
|
||||
// Init widget
|
||||
let widget = Viewport::builder().scroll_to_focus(false).build();
|
||||
|
||||
widget.set_child(Some(reader.widget()));
|
||||
|
||||
// Result
|
||||
Self { reader, widget }
|
||||
}
|
||||
|
||||
// Getters
|
||||
pub fn reader_title(&self) -> &Option<GString> {
|
||||
&self.reader.title()
|
||||
}
|
||||
|
||||
pub fn widget(&self) -> &Viewport {
|
||||
&self.widget
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
mod parser;
|
||||
|
||||
use parser::header::Header;
|
||||
use parser::link::Link;
|
||||
use parser::plain::Plain;
|
||||
|
||||
use gtk::{
|
||||
gio::SimpleAction,
|
||||
glib::{GString, Propagation, Uri, UriFlags},
|
||||
prelude::{ActionExt, StyleContextExt, ToVariant, WidgetExt},
|
||||
Align, CssProvider, Label, STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Reader {
|
||||
title: Option<GString>,
|
||||
// css: CssProvider,
|
||||
widget: Label,
|
||||
}
|
||||
|
||||
impl Reader {
|
||||
// Construct
|
||||
pub fn new(gemtext: &str, base: &Uri, action_page_open: Arc<SimpleAction>) -> Self {
|
||||
// Init title
|
||||
let mut title = None;
|
||||
|
||||
// Init markup
|
||||
let mut markup = String::new();
|
||||
|
||||
for line in gemtext.lines() {
|
||||
// Is header
|
||||
if let Some(header) = Header::from(line) {
|
||||
// Format
|
||||
markup.push_str(header.markup());
|
||||
|
||||
// Set title from first document header tag
|
||||
if title == None {
|
||||
title = Some(header.text().clone());
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Is link
|
||||
if let Some(link) = Link::from(line, base) {
|
||||
// Format
|
||||
markup.push_str(link.markup());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Nothing match, escape string just
|
||||
markup.push_str(Plain::from(line).markup())
|
||||
}
|
||||
|
||||
// Init CSS
|
||||
let css = CssProvider::new();
|
||||
|
||||
/* @TODO Theme parser error: <broken file>
|
||||
css.load_from_path(
|
||||
"src/browser/main/tab/page/content/text/gemini/reader/default.css"
|
||||
); */
|
||||
|
||||
css.load_from_data("label{caret-color: transparent;}");
|
||||
|
||||
// Init widget
|
||||
let widget = Label::builder()
|
||||
.halign(Align::Fill)
|
||||
.valign(Align::Fill)
|
||||
.hexpand(true)
|
||||
.vexpand(true)
|
||||
.xalign(0.0)
|
||||
.yalign(0.0)
|
||||
.margin_start(8)
|
||||
.margin_end(8)
|
||||
.wrap(true)
|
||||
.selectable(true)
|
||||
.use_markup(true)
|
||||
.label(markup)
|
||||
.build();
|
||||
|
||||
widget
|
||||
.style_context()
|
||||
.add_provider(&css, STYLE_PROVIDER_PRIORITY_APPLICATION);
|
||||
|
||||
// Connect actions
|
||||
widget.connect_activate_link(move |_, href| {
|
||||
// Detect requested protocol
|
||||
if let Ok(uri) = Uri::parse(&href, UriFlags::NONE) {
|
||||
return match uri.scheme().as_str() {
|
||||
"gemini" => {
|
||||
// Open new page
|
||||
action_page_open.activate(Some(&uri.to_str().to_variant()));
|
||||
|
||||
// Prevent link open in external application
|
||||
Propagation::Stop
|
||||
}
|
||||
// Protocol not supported
|
||||
_ => Propagation::Proceed,
|
||||
};
|
||||
}
|
||||
|
||||
// Delegate unparsable
|
||||
Propagation::Proceed
|
||||
});
|
||||
|
||||
// Result
|
||||
Self {
|
||||
title,
|
||||
// css,
|
||||
widget,
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
pub fn title(&self) -> &Option<GString> {
|
||||
&self.title
|
||||
}
|
||||
|
||||
pub fn widget(&self) -> &Label {
|
||||
&self.widget
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/* @TODO
|
||||
* not in use as defined inline:
|
||||
* src/browser/main/tab/page/content/text/gemini/reader.rs
|
||||
*/
|
||||
label
|
||||
{
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
pub mod header;
|
||||
pub mod link;
|
||||
pub mod plain;
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
use gtk::glib::{gformat, markup_escape_text, GString, Regex, RegexCompileFlags, RegexMatchFlags};
|
||||
|
||||
pub enum Level {
|
||||
H1,
|
||||
H2,
|
||||
H3,
|
||||
}
|
||||
|
||||
pub struct Header {
|
||||
// level: Level,
|
||||
text: GString,
|
||||
markup: GString,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
pub fn from(line: &str) -> Option<Header> {
|
||||
// Parse line
|
||||
let parsed = Regex::split_simple(
|
||||
r"^(#{1,3})\s*(.+)$",
|
||||
line,
|
||||
RegexCompileFlags::DEFAULT,
|
||||
RegexMatchFlags::DEFAULT,
|
||||
);
|
||||
|
||||
// Validate match results
|
||||
if let Some(text) = parsed.get(2) {
|
||||
if let Some(level) = parsed.get(1) {
|
||||
// Init level
|
||||
let level = match level.len() {
|
||||
1 => Level::H1,
|
||||
2 => Level::H2,
|
||||
3 => Level::H3,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// Init text
|
||||
let text = GString::from(text.as_str());
|
||||
|
||||
if text.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Init markup
|
||||
let markup = match level {
|
||||
Level::H1 => gformat!(
|
||||
"<span size=\"xx-large\">{}</span>\n",
|
||||
markup_escape_text(&text)
|
||||
),
|
||||
Level::H2 => gformat!(
|
||||
"<span size=\"x-large\">{}</span>\n",
|
||||
markup_escape_text(&text)
|
||||
),
|
||||
Level::H3 => gformat!(
|
||||
"<span size=\"large\">{}</span>\n",
|
||||
markup_escape_text(&text)
|
||||
),
|
||||
};
|
||||
|
||||
// Result
|
||||
return Some(Header {
|
||||
// level,
|
||||
text,
|
||||
markup,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None // not header line given
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &GString {
|
||||
&self.text
|
||||
}
|
||||
|
||||
pub fn markup(&self) -> &GString {
|
||||
&self.markup
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
use gtk::glib::{
|
||||
gformat, markup_escape_text, GString, Regex, RegexCompileFlags, RegexMatchFlags, Uri, UriFlags,
|
||||
};
|
||||
|
||||
pub struct Link {
|
||||
// alt: Option<GString>, // [optional] alternative text
|
||||
// date: Option<GString>, // [optional] date @TODO store in UnixTime?
|
||||
// external: bool, // external link indicator
|
||||
// link: GString, // original link, wanted for title tooltip
|
||||
markup: GString, // pango markup with escaped special chars
|
||||
// uri: Uri, // parsed link object (currently not in use)
|
||||
}
|
||||
|
||||
impl Link {
|
||||
// Link structure parser
|
||||
// line - gemtext subject to parse
|
||||
// base - Uri object, required for:
|
||||
// 1. relative to absolute address conversion
|
||||
// 2. external links indication
|
||||
// returns new Link struct or None
|
||||
pub fn from(line: &str, base: &Uri) -> Option<Link> {
|
||||
// Init struct members
|
||||
// let mut alt: Option<GString> = None;
|
||||
// let mut date: Option<GString> = None;
|
||||
let external: bool;
|
||||
let link: GString;
|
||||
let markup: GString;
|
||||
let uri: Uri;
|
||||
|
||||
// Parse line
|
||||
let parsed = Regex::split_simple(
|
||||
r"^=>\s*([^\s]+)\s*(\d{4}-\d{2}-\d{2})?\s*(.+)?$",
|
||||
line,
|
||||
RegexCompileFlags::DEFAULT,
|
||||
RegexMatchFlags::DEFAULT,
|
||||
);
|
||||
|
||||
// Address
|
||||
match parsed.get(1) {
|
||||
Some(address) => {
|
||||
// Define original link value (used in titles or when alt is empty)
|
||||
link = GString::from(address.as_str());
|
||||
// Links in document usually relative, make them absolute to base given
|
||||
match Uri::resolve_relative(Some(&base.to_str()), address.as_str(), UriFlags::NONE)
|
||||
{
|
||||
Ok(resolved) => {
|
||||
// Make URI parsed as always valid (no idea why does lib operate strings, not objects)
|
||||
match Uri::parse(&resolved, UriFlags::NONE) {
|
||||
Ok(object) => {
|
||||
// Set external status
|
||||
external =
|
||||
object.host() != base.host() || object.port() != base.port();
|
||||
|
||||
// Set struct URI
|
||||
uri = object;
|
||||
}
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
None => return None,
|
||||
}
|
||||
|
||||
// Create link name based on external status, date and alt values
|
||||
let mut name = Vec::new();
|
||||
|
||||
if external {
|
||||
name.push("⇖".to_string());
|
||||
}
|
||||
|
||||
// Date
|
||||
if let Some(this) = parsed.get(2) {
|
||||
// date = Some(GString::from(this.to_string()));
|
||||
name.push(this.to_string());
|
||||
}
|
||||
|
||||
// Alt
|
||||
match parsed.get(3) {
|
||||
// Not empty
|
||||
Some(this) => {
|
||||
// alt = Some(GString::from(this.to_string()));
|
||||
name.push(this.to_string());
|
||||
}
|
||||
// Empty, use resolved address
|
||||
None => name.push(link.to_string()),
|
||||
};
|
||||
|
||||
// Markup
|
||||
markup = gformat!(
|
||||
"<a href=\"{}\" title=\"{}\"><span underline=\"none\">{}</span></a>\n",
|
||||
markup_escape_text(&uri.to_str()), // use resolved address for href
|
||||
markup_escape_text(&link), // show original address for title
|
||||
markup_escape_text(&name.join(" ")),
|
||||
);
|
||||
|
||||
Some(Self {
|
||||
// alt,
|
||||
// date,
|
||||
// external,
|
||||
// link,
|
||||
markup,
|
||||
// uri,
|
||||
})
|
||||
}
|
||||
|
||||
// Getters
|
||||
/* @TODO
|
||||
pub fn alt(&self) -> &Option<GString> {
|
||||
&self.alt
|
||||
}
|
||||
|
||||
pub fn date(&self) -> &Option<GString> {
|
||||
&self.date
|
||||
}
|
||||
|
||||
pub fn external(&self) -> &bool {
|
||||
&self.external
|
||||
}
|
||||
|
||||
pub fn link(&self) -> &GString {
|
||||
&self.link
|
||||
}
|
||||
|
||||
pub fn uri(&self) -> &Uri {
|
||||
&self.uri
|
||||
}*/
|
||||
|
||||
pub fn markup(&self) -> &GString {
|
||||
&self.markup
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
use gtk::glib::{gformat, markup_escape_text, GString};
|
||||
|
||||
pub struct Plain {
|
||||
markup: GString,
|
||||
}
|
||||
|
||||
impl Plain {
|
||||
pub fn from(line: &str) -> Plain {
|
||||
Self {
|
||||
markup: gformat!("{}\n", markup_escape_text(line)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn markup(&self) -> &GString {
|
||||
&self.markup
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
use gtk::{Align, Label};
|
||||
|
||||
pub struct Reader {
|
||||
widget: Label,
|
||||
}
|
||||
|
||||
impl Reader {
|
||||
// Construct
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
widget: Label::builder()
|
||||
.halign(Align::Start)
|
||||
.valign(Align::Start)
|
||||
.margin_start(8)
|
||||
.margin_end(8)
|
||||
.wrap(true)
|
||||
.selectable(true)
|
||||
.use_markup(true)
|
||||
.build(),
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
pub fn widget(&self) -> &Label {
|
||||
&self.widget
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue