create parental app mod

This commit is contained in:
yggverse 2024-10-02 02:14:00 +03:00
parent 2461f2a0fb
commit b2aa3af46a
37 changed files with 176 additions and 111 deletions

24
src/app/browser/db.rs Normal file
View file

@ -0,0 +1,24 @@
/* @TODO
use std::sync::Arc;
pub struct Browser {
connection: Arc<sqlite::Connection>,
}
impl Browser {
// Construct new browser DB (connection)
pub fn new(connection: Arc<sqlite::Connection>) -> Browser {
let this = Self { connection };
this.init();
this
}
// Create browser table in DB if not exist yet
fn init(&self) {}
// Save active browser session to DB
fn save(&self) {}
// Restore previous browser session from DB
fn restore(&self) {}
}*/

64
src/app/browser/header.rs Normal file
View file

@ -0,0 +1,64 @@
mod subject;
mod tray;
use subject::Subject;
use tray::Tray;
use gtk::{gio::SimpleAction, glib::GString, HeaderBar};
use std::sync::Arc;
pub struct Header {
widget: HeaderBar,
subject: Subject,
}
impl Header {
// Construct
pub fn new(
action_debug: Arc<SimpleAction>,
action_quit: Arc<SimpleAction>,
action_tab_append: Arc<SimpleAction>,
action_tab_close: Arc<SimpleAction>,
action_tab_close_all: Arc<SimpleAction>,
action_tab_page_navigation_base: Arc<SimpleAction>,
action_tab_page_navigation_history_back: Arc<SimpleAction>,
action_tab_page_navigation_history_forward: Arc<SimpleAction>,
action_tab_page_navigation_reload: Arc<SimpleAction>,
action_tab_pin: Arc<SimpleAction>,
) -> Self {
// Init components
let tray = Tray::new(
action_debug,
action_quit,
action_tab_append,
action_tab_close,
action_tab_close_all,
action_tab_page_navigation_base,
action_tab_page_navigation_history_back,
action_tab_page_navigation_history_forward,
action_tab_page_navigation_reload,
action_tab_pin,
);
let subject = Subject::new();
// Init widget
let widget = HeaderBar::builder().build();
widget.pack_start(tray.widget());
widget.set_title_widget(Some(subject.widget()));
// Return new struct
Self { widget, subject }
}
// Actions
pub fn update(&self, title: Option<GString>, description: Option<GString>) {
self.subject.update(title, description);
}
// Getters
pub fn widget(&self) -> &HeaderBar {
&self.widget
}
}

View file

@ -0,0 +1,46 @@
mod description;
mod title;
use description::Description;
use title::Title;
use gtk::{glib::GString, prelude::BoxExt, Align, Box, Orientation};
pub struct Subject {
widget: Box,
title: Title,
description: Description,
}
impl Subject {
// Construct
pub fn new() -> Self {
let title = Title::new();
let description = Description::new();
let widget = Box::builder()
.orientation(Orientation::Vertical)
.valign(Align::Center)
.build();
widget.append(title.widget());
widget.append(description.widget());
Self {
widget,
title,
description,
}
}
// Actions
pub fn update(&self, title: Option<GString>, description: Option<GString>) {
self.title.update(title);
self.description.update(description);
}
// Getters
pub fn widget(&self) -> &Box {
&self.widget
}
}

View file

@ -0,0 +1,35 @@
use gtk::glib::GString;
use gtk::prelude::WidgetExt;
use gtk::{pango::EllipsizeMode, Label};
pub struct Description {
widget: Label,
}
impl Description {
// Construct
pub fn new() -> Self {
let widget = Label::builder()
.css_classes(["subtitle"])
.single_line_mode(true)
.ellipsize(EllipsizeMode::End)
.visible(false)
.build();
Self { widget }
}
// Actions
pub fn update(&self, text: Option<GString>) {
match text {
Some(value) => self.widget.set_text(&value),
None => self.widget.set_text(""), // @TODO
};
self.widget.set_visible(!self.widget.text().is_empty());
}
// Getters
pub fn widget(&self) -> &Label {
&self.widget
}
}

View file

@ -0,0 +1,41 @@
use gtk::{glib::GString, pango::EllipsizeMode, Label};
const DEFAULT_TEXT: &str = "Yoda"; // @TODO
pub struct Title {
widget: Label,
}
impl Title {
// Construct
pub fn new() -> Self {
let widget = gtk::Label::builder()
.css_classes(["title"])
.single_line_mode(true)
.ellipsize(EllipsizeMode::End)
.label(DEFAULT_TEXT)
.build();
Self { widget }
}
// Actions
pub fn update(&self, text: Option<GString>) {
let mut name = Vec::new();
if let Some(value) = text {
if !value.is_empty() {
name.push(value);
}
}
name.push(GString::from(DEFAULT_TEXT));
self.widget.set_text(&name.join(" - "));
}
// Getters
pub fn widget(&self) -> &Label {
&self.widget
}
}

View file

@ -0,0 +1,65 @@
mod menu;
mod tab;
use menu::Menu;
use tab::Tab;
use gtk::{
gio::SimpleAction,
prelude::BoxExt,
{Box, Orientation},
};
use std::sync::Arc;
pub struct Tray {
widget: Box,
}
impl Tray {
pub fn new(
action_debug: Arc<SimpleAction>,
action_quit: Arc<SimpleAction>,
action_tab_append: Arc<SimpleAction>,
action_tab_close: Arc<SimpleAction>,
action_tab_close_all: Arc<SimpleAction>,
action_tab_page_navigation_base: Arc<SimpleAction>,
action_tab_page_navigation_history_back: Arc<SimpleAction>,
action_tab_page_navigation_history_forward: Arc<SimpleAction>,
action_tab_page_navigation_reload: Arc<SimpleAction>,
action_tab_pin: Arc<SimpleAction>,
) -> Self {
// Init components
let tab = Tab::new(action_tab_append.clone());
let menu = Menu::new(
action_debug,
action_quit,
action_tab_append,
action_tab_close,
action_tab_close_all,
action_tab_page_navigation_base,
action_tab_page_navigation_history_back,
action_tab_page_navigation_history_forward,
action_tab_page_navigation_reload,
action_tab_pin,
);
// Init widget
let widget = Box::builder()
.orientation(Orientation::Horizontal)
.spacing(8)
.build();
widget.append(menu.widget());
widget.append(tab.widget());
// Return new struct
Self { widget }
}
// Getters
pub fn widget(&self) -> &Box {
&self.widget
}
}

View file

@ -0,0 +1,80 @@
use gtk::{
gio::{self, SimpleAction},
glib::{gformat, GString},
prelude::ActionExt,
MenuButton,
};
use std::sync::Arc;
pub struct Menu {
widget: MenuButton,
}
#[rustfmt::skip] // @TODO template builder?
impl Menu {
pub fn new(
action_debug: Arc<SimpleAction>,
action_quit: Arc<SimpleAction>,
action_tab_append: Arc<SimpleAction>,
action_tab_close: Arc<SimpleAction>,
action_tab_close_all: Arc<SimpleAction>,
action_tab_page_navigation_base: Arc<SimpleAction>,
action_tab_page_navigation_history_back: Arc<SimpleAction>,
action_tab_page_navigation_history_forward: Arc<SimpleAction>,
action_tab_page_navigation_reload: Arc<SimpleAction>,
action_tab_pin: Arc<SimpleAction>,
) -> Self {
// Init model
let model = gio::Menu::new();
let model_tab = gio::Menu::new();
model_tab.append(Some("New"), Some(&detailed_action_name(action_tab_append)));
model_tab.append(Some("Pin"), Some(&detailed_action_name(action_tab_pin)));
let model_tab_page = gio::Menu::new();
let model_tab_page_navigation = gio::Menu::new();
model_tab_page_navigation.append(Some("Base"), Some(&detailed_action_name(action_tab_page_navigation_base)));
let model_tab_page_navigation_history = gio::Menu::new();
model_tab_page_navigation_history.append(Some("Back"), Some(&detailed_action_name(action_tab_page_navigation_history_back)));
model_tab_page_navigation_history.append(Some("Forward"), Some(&detailed_action_name(action_tab_page_navigation_history_forward)));
model_tab_page_navigation.append_submenu(Some("History"), &model_tab_page_navigation_history);
model_tab_page_navigation.append(Some("Reload"), Some(&detailed_action_name(action_tab_page_navigation_reload)));
// @TODO model_tab_page_navigation.append(Some("Bookmark"), Some("win.tab_page_bookmark"));
model_tab_page.append_submenu(Some("Navigation"), &model_tab_page_navigation);
model_tab.append_submenu(Some("Page"), &model_tab_page);
let model_tab_close = gio::Menu::new();
model_tab_close.append(Some("Current"), Some(&detailed_action_name(action_tab_close)));
model_tab_close.append(Some("All"), Some(&detailed_action_name(action_tab_close_all)));
model_tab.append_submenu(Some("Close"), &model_tab_close);
model.append_submenu(Some("Tab"), &model_tab);
model.append(Some("Debug"), Some(&detailed_action_name(action_debug)));
model.append(Some("Quit"), Some(&detailed_action_name(action_quit)));
// Init widget
let widget = MenuButton::builder().tooltip_text("Menu").build();
widget.set_menu_model(Some(&model));
// Result
Self { widget }
}
// Getters
pub fn widget(&self) -> &MenuButton {
&self.widget
}
}
// Private helpers
fn detailed_action_name(action: Arc<SimpleAction>) -> GString {
gformat!("win.{}", action.name()) // @TODO find the way to ident parent group
// without application-wide dependencies import
// see also src/app/action.rs
}

View file

@ -0,0 +1,30 @@
use gtk::{gio::SimpleAction, prelude::ActionExt, prelude::ButtonExt, Button};
use std::sync::Arc;
pub struct Tab {
pub widget: Button,
}
impl Tab {
// Construct
pub fn new(action_tab_append: Arc<SimpleAction>) -> Self {
// Init widget
let widget = Button::builder()
.icon_name("tab-new-symbolic")
.tooltip_text("New tab")
.build();
// Init events
widget.connect_clicked(move |_| {
action_tab_append.activate(None);
});
// Return activated struct
Self { widget }
}
// Getters
pub fn widget(&self) -> &Button {
&self.widget
}
}

92
src/app/browser/main.rs Normal file
View file

@ -0,0 +1,92 @@
mod tab;
use std::sync::Arc;
use tab::Tab;
use gtk::{gio::SimpleAction, glib::GString, prelude::BoxExt, Box, Orientation};
pub struct Main {
tab: Arc<Tab>,
widget: Box,
}
impl Main {
// Construct
pub fn new(
action_tab_page_navigation_base: Arc<SimpleAction>,
action_tab_page_navigation_history_back: Arc<SimpleAction>,
action_tab_page_navigation_history_forward: Arc<SimpleAction>,
action_tab_page_navigation_reload: Arc<SimpleAction>,
action_update: Arc<SimpleAction>,
) -> Self {
// Init components
let tab = Arc::new(Tab::new(
action_tab_page_navigation_base,
action_tab_page_navigation_history_back,
action_tab_page_navigation_history_forward,
action_tab_page_navigation_reload,
action_update,
));
tab.activate(tab.clone());
tab.append(Some(GString::from("gemini://geminiprotocol.net/")), true); // demo tab @TODO replace with session restore feature
// GTK
let widget = Box::builder().orientation(Orientation::Vertical).build();
widget.append(tab.widget());
// Init struct
Self { tab, widget }
}
// Actions
pub fn tab_append(&self, tab_page_navigation_request_text: Option<GString>) {
self.tab.append(tab_page_navigation_request_text, true);
}
pub fn tab_page_navigation_base(&self) {
self.tab.page_navigation_base();
}
pub fn tab_page_navigation_history_back(&self) {
self.tab.page_navigation_history_back();
}
pub fn tab_page_navigation_history_forward(&self) {
self.tab.page_navigation_history_forward();
}
pub fn tab_page_navigation_reload(&self) {
self.tab.page_navigation_reload();
}
pub fn tab_close(&self) {
self.tab.close();
}
pub fn tab_close_all(&self) {
self.tab.close_all();
}
pub fn tab_pin(&self) {
self.tab.pin();
}
pub fn update(&self) {
self.tab.update();
}
// Getters
pub fn tab_page_title(&self) -> Option<GString> {
self.tab.page_title()
}
pub fn tab_page_description(&self) -> Option<GString> {
self.tab.page_description()
}
pub fn widget(&self) -> &Box {
&self.widget
}
}

274
src/app/browser/main/tab.rs Normal file
View file

@ -0,0 +1,274 @@
mod label;
mod page;
use label::Label;
use page::Page;
use gtk::{
gio::SimpleAction,
glib::{uuid_string_random, GString},
prelude::{ActionExt, WidgetExt},
GestureClick, Notebook,
};
use std::{cell::RefCell, collections::HashMap, sync::Arc};
pub struct Tab {
// GTK
widget: Notebook,
// Keep action links in memory to not require them on every tab append
action_tab_page_navigation_base: Arc<SimpleAction>,
action_tab_page_navigation_history_back: Arc<SimpleAction>,
action_tab_page_navigation_history_forward: Arc<SimpleAction>,
action_tab_page_navigation_reload: Arc<SimpleAction>,
action_update: Arc<SimpleAction>,
// Dynamically allocated reference index
labels: RefCell<HashMap<GString, Arc<Label>>>,
pages: RefCell<HashMap<GString, Arc<Page>>>,
}
impl Tab {
// Construct
pub fn new(
action_tab_page_navigation_base: Arc<SimpleAction>,
action_tab_page_navigation_history_back: Arc<SimpleAction>,
action_tab_page_navigation_history_forward: Arc<SimpleAction>,
action_tab_page_navigation_reload: Arc<SimpleAction>,
action_update: Arc<SimpleAction>,
) -> Self {
// Init widget
let widget = Notebook::builder().scrollable(true).build();
// Return non activated struct
Self {
// GTK
widget,
// Define action links
action_tab_page_navigation_base,
action_tab_page_navigation_history_back,
action_tab_page_navigation_history_forward,
action_tab_page_navigation_reload,
action_update,
// Init empty HashMap index as no tabs appended yet
labels: RefCell::new(HashMap::new()),
pages: RefCell::new(HashMap::new()),
}
}
// Actions
pub fn activate(&self, tab: Arc<Self>) {
self.widget.connect_page_removed(move |_, widget, _| {
// Cleanup HashMap index
let id = &widget.widget_name();
tab.labels.borrow_mut().remove(id);
tab.pages.borrow_mut().remove(id);
});
// Switch page post-event (`connect_switch_page` activates before `page_number` get updated)
self.widget.connect_page_notify({
let action_update = self.action_update.clone();
// Update window header with current page title
move |_| action_update.activate(None)
});
}
pub fn append(
&self,
page_navigation_request_text: Option<GString>,
is_current_page: bool,
) -> u32 {
// Generate unique ID for new page components
let id = uuid_string_random();
// Init new tab components
let label = Arc::new(Label::new(id.clone(), false));
let page = Arc::new(Page::new(
id.clone(),
page_navigation_request_text.clone(),
self.action_tab_page_navigation_base.clone(),
self.action_tab_page_navigation_history_back.clone(),
self.action_tab_page_navigation_history_forward.clone(),
self.action_tab_page_navigation_reload.clone(),
self.action_update.clone(),
));
// Register dynamically created tab components in the HashMap index
self.labels.borrow_mut().insert(id.clone(), label.clone());
self.pages.borrow_mut().insert(id.clone(), page.clone());
// Init additional label actions
let controller = GestureClick::new();
controller.connect_pressed({
let label = label.clone();
move |_, n: i32, _, _| {
// double click
if n == 2 {
label.pin(!label.is_pinned()); // toggle
}
}
});
label.widget().add_controller(controller);
// Append new Notebook page
let page_number = self.widget.append_page(page.widget(), Some(label.widget()));
// Additional setup for Notebook tab created
self.widget.set_tab_reorderable(page.widget(), true);
if is_current_page {
self.widget.set_current_page(Some(page_number));
}
if page_navigation_request_text.is_none() {
page.navigation_request_grab_focus();
}
// Result
page_number
}
// Close active tab
pub fn close(&self) {
self.widget.remove_page(self.widget.current_page());
}
// Close all tabs
pub fn close_all(&self) {
// @TODO skip pinned or make confirmation alert (GTK>=4.10)
while let Some(page_number) = self.widget.current_page() {
self.widget.remove_page(Some(page_number));
}
}
// Toggle pin status for active tab
pub fn pin(&self) {
// Get current page
if let Some(page_number) = self.widget.current_page() {
// Get default widget to extract it name as the ID for childs
if let Some(widget) = self.widget.nth_page(Some(page_number)) {
// Get label by ID
if let Some(label) = self.labels.borrow().get(&widget.widget_name()) {
label.pin(!label.is_pinned()); // toggle
}
}
}
}
pub fn page_navigation_base(&self) {
// Get current page
if let Some(page_number) = self.widget.current_page() {
// Get default widget to extract it name as the ID for childs
if let Some(widget) = self.widget.nth_page(Some(page_number)) {
// Get page by widget ID
if let Some(page) = self.pages.borrow().get(&widget.widget_name()) {
page.navigation_base();
}
}
}
}
pub fn page_navigation_history_back(&self) {
// Get current page
if let Some(page_number) = self.widget.current_page() {
// Get default widget to extract it name as the ID for childs
if let Some(widget) = self.widget.nth_page(Some(page_number)) {
// Get page by widget ID
if let Some(page) = self.pages.borrow().get(&widget.widget_name()) {
page.navigation_history_back();
}
}
}
}
pub fn page_navigation_history_forward(&self) {
// Get current page
if let Some(page_number) = self.widget.current_page() {
// Get default widget to extract it name as the ID for childs
if let Some(widget) = self.widget.nth_page(Some(page_number)) {
// Get page by widget ID
if let Some(page) = self.pages.borrow().get(&widget.widget_name()) {
page.navigation_history_forward();
}
}
}
}
pub fn page_navigation_reload(&self) {
// Get current page
if let Some(page_number) = self.widget.current_page() {
// Get default widget to extract it name as the ID for childs
if let Some(widget) = self.widget.nth_page(Some(page_number)) {
// Get page by widget ID
if let Some(page) = self.pages.borrow().get(&widget.widget_name()) {
page.navigation_reload();
}
}
}
}
pub fn update(&self) {
// Get current page
if let Some(page_number) = self.widget.current_page() {
// Get default widget to extract it name as the ID for childs
if let Some(widget) = self.widget.nth_page(Some(page_number)) {
// Get widget ID
let id = &widget.widget_name();
// Get page by widget ID
if let Some(page) = self.pages.borrow().get(id) {
page.update();
// Get label by widget ID
if let Some(label) = self.labels.borrow().get(id) {
if let Some(title) = page.title() {
label.update(Some(&title));
} else {
label.update(None);
}
}
}
}
}
}
// Getters
pub fn page_title(&self) -> Option<GString> {
// Get current page
if let Some(page_number) = self.widget.current_page() {
// Get default widget to extract it name as the ID for childs
if let Some(widget) = self.widget.nth_page(Some(page_number)) {
// Get widget ID
let id = &widget.widget_name();
// Get page by widget ID
if let Some(page) = self.pages.borrow().get(id) {
return page.title();
}
}
}
None
}
pub fn page_description(&self) -> Option<GString> {
// Get current page
if let Some(page_number) = self.widget.current_page() {
// Get default widget to extract it name as the ID for childs
if let Some(widget) = self.widget.nth_page(Some(page_number)) {
// Get widget ID
let id = &widget.widget_name();
// Get page by widget ID
if let Some(page) = self.pages.borrow().get(id) {
return page.description();
}
}
}
None
}
pub fn widget(&self) -> &Notebook {
self.widget.as_ref()
}
}

View file

@ -0,0 +1,68 @@
mod pin;
mod title;
use pin::Pin;
use title::Title;
use gtk::{
glib::GString,
prelude::{BoxExt, WidgetExt},
Align, Box, Orientation,
};
pub struct Label {
// Components
pin: Pin,
title: Title,
// GTK
widget: Box,
}
impl Label {
// Construct
pub fn new(name: GString, is_pinned: bool) -> Label {
// Components
let pin = Pin::new(is_pinned);
let title = Title::new();
// GTK
let widget = Box::builder()
.orientation(Orientation::Horizontal)
.halign(Align::Center)
.name(name)
.tooltip_text(title.widget().text())
.build();
widget.append(pin.widget());
widget.append(title.widget());
// Result
Self { pin, title, widget }
}
// Actions
pub fn update(&self, title: Option<&GString>) {
match title {
Some(tooltip_text) => self.widget.set_tooltip_text(Some(tooltip_text)),
None => self.widget.set_tooltip_text(None),
}
self.title.update(title);
}
// Setters
pub fn pin(&self, is_pinned: bool) {
self.pin.widget().set_visible(is_pinned);
self.title.widget().set_visible(!is_pinned);
}
// Getters
pub fn is_pinned(&self) -> bool {
self.pin.widget().is_visible()
}
pub fn widget(&self) -> &Box {
&self.widget
}
}

View file

@ -0,0 +1,22 @@
use gtk::Image;
pub struct Pin {
widget: Image,
}
impl Pin {
// Construct
pub fn new(visible: bool) -> Pin {
let widget = Image::builder()
.icon_name("view-pin-symbolic")
.visible(visible)
.build();
Self { widget }
}
// Getters
pub fn widget(&self) -> &Image {
&self.widget
}
}

View file

@ -0,0 +1,34 @@
use gtk::{glib::GString, pango::EllipsizeMode, Label};
const DEFAULT_LABEL_TEXT: &str = "New page";
pub struct Title {
widget: Label,
}
impl Title {
// Construct
pub fn new() -> Self {
Self {
widget: Label::builder()
.label(DEFAULT_LABEL_TEXT)
.ellipsize(EllipsizeMode::End)
.width_chars(16)
.single_line_mode(true)
.build(),
}
}
// Actions
pub fn update(&self, title: Option<&GString>) {
match title {
Some(title) => self.widget.set_text(title),
None => self.widget.set_text(DEFAULT_LABEL_TEXT),
}
}
// Getters
pub fn widget(&self) -> &Label {
&self.widget
}
}

View file

@ -0,0 +1,489 @@
mod content;
mod meta;
mod navigation;
use content::Content;
use meta::{Meta, Mime, Status};
use navigation::Navigation;
use gtk::{
gio::{
Cancellable, SimpleAction, SimpleActionGroup, SocketClient, SocketProtocol,
TlsCertificateFlags,
},
glib::{gformat, GString, Priority, Regex, RegexCompileFlags, RegexMatchFlags, Uri, UriFlags},
prelude::{
ActionExt, ActionMapExt, BoxExt, IOStreamExt, InputStreamExtManual, OutputStreamExtManual,
SocketClientExt, StaticVariantType, ToVariant, WidgetExt,
},
Box, Orientation,
};
use std::{cell::RefCell, path::Path, sync::Arc};
pub struct Page {
// GTK
widget: Box,
// Actions
action_page_open: Arc<SimpleAction>,
action_tab_page_navigation_reload: Arc<SimpleAction>,
action_update: Arc<SimpleAction>,
// Components
navigation: Arc<Navigation>,
content: Arc<Content>,
// Extras
meta: Arc<RefCell<Meta>>,
}
impl Page {
// Construct
pub fn new(
name: GString,
navigation_request_text: Option<GString>,
action_tab_page_navigation_base: Arc<SimpleAction>,
action_tab_page_navigation_history_back: Arc<SimpleAction>,
action_tab_page_navigation_history_forward: Arc<SimpleAction>,
action_tab_page_navigation_reload: Arc<SimpleAction>,
action_update: Arc<SimpleAction>,
) -> Page {
// Init actions
let action_page_open = Arc::new(SimpleAction::new(
"open",
Some(&String::static_variant_type()),
));
// Init action group
let action_group = SimpleActionGroup::new();
action_group.add_action(action_page_open.as_ref());
// Init components
let content = Arc::new(Content::new(action_page_open.clone()));
let navigation = Arc::new(Navigation::new(
navigation_request_text,
action_tab_page_navigation_base.clone(),
action_tab_page_navigation_history_back.clone(),
action_tab_page_navigation_history_forward.clone(),
action_tab_page_navigation_reload.clone(),
action_update.clone(),
));
// Init widget
let widget = Box::builder()
.orientation(Orientation::Vertical)
.name(name)
.build();
widget.append(navigation.widget());
widget.append(content.widget());
widget.insert_action_group("page", Some(&action_group));
// Init async mutable Meta object
let meta = Arc::new(RefCell::new(Meta::new()));
// Init events
action_page_open.connect_activate({
let navigation = navigation.clone();
let action_tab_page_navigation_reload = action_tab_page_navigation_reload.clone();
move |_, request| {
// Convert to GString
let request = GString::from(
request
.expect("Parameter required for `page.open` action")
.get::<String>()
.expect("Parameter does not match `String`"),
);
// Update
navigation.set_request_text(&request);
// Reload page
action_tab_page_navigation_reload.activate(None);
}
});
// Return activated structure
Self {
// GTK
widget,
// Actions
action_page_open,
action_tab_page_navigation_reload,
action_update,
// Components
content,
navigation,
// Extras
meta,
}
}
// Actions
pub fn navigation_request_grab_focus(&self) {
self.navigation.request_grab_focus();
}
pub fn navigation_base(&self) {
if let Some(url) = self.navigation.base_url() {
// Update with history record
self.action_page_open.activate(Some(&url.to_variant()));
}
}
pub fn navigation_history_back(&self) {
if let Some(request) = self.navigation.history_back(true) {
// Update
self.navigation.set_request_text(&request);
// Reload page
self.action_tab_page_navigation_reload.activate(None);
}
}
pub fn navigation_history_forward(&self) {
if let Some(request) = self.navigation.history_forward(true) {
// Update
self.navigation.set_request_text(&request);
// Reload page
self.action_tab_page_navigation_reload.activate(None);
}
}
pub fn navigation_reload(&self) {
// Init globals
let request_text = self.navigation.request_text();
// Init shared objects for async access
let navigation = self.navigation.clone();
let content = self.content.clone();
let meta = self.meta.clone();
let action_update = self.action_update.clone();
// Update
meta.borrow_mut().mime = None;
meta.borrow_mut().status = Some(Status::Reload);
meta.borrow_mut().title = Some(gformat!("Loading.."));
meta.borrow_mut().description = None;
action_update.activate(None);
/*let _uri = */
match Uri::parse(&request_text, UriFlags::NONE) {
Ok(uri) => {
// Route request by scheme
match uri.scheme().as_str() {
"file" => {
todo!()
}
"gemini" => {
// Get host
let host = match uri.host() {
Some(host) => host,
None => panic!(),
};
// Update
meta.borrow_mut().status = Some(Status::Prepare);
meta.borrow_mut().description = Some(gformat!("Connect {host}.."));
action_update.activate(None);
// Create new connection
let cancellable = Cancellable::new();
let client = SocketClient::new();
client.set_timeout(10);
client.set_tls(true);
client.set_tls_validation_flags(TlsCertificateFlags::INSECURE);
client.set_protocol(SocketProtocol::Tcp);
client.connect_to_uri_async(
&uri.to_str(),
1965,
Some(&cancellable.clone()),
move |result| match result {
Ok(connection) => {
// Update
meta.borrow_mut().status = Some(Status::Connect);
meta.borrow_mut().description = Some(gformat!("Connected to {host}.."));
action_update.activate(None);
// Send request
connection.output_stream().write_all_async(
gformat!("{}\r\n", &uri.to_str()),
Priority::DEFAULT,
Some(&cancellable.clone()),
move |result| match result {
Ok(_) => {
// Update
meta.borrow_mut().status = Some(Status::Request);
meta.borrow_mut().description = Some(gformat!("Request data from {host}.."));
action_update.activate(None);
// Read response
connection.input_stream().read_all_async(
vec![0; 0xfffff], // 1Mb @TODO
Priority::DEFAULT,
Some(&cancellable.clone()),
move |result| match result {
Ok(response) => {
match GString::from_utf8_until_nul(
response.0,
) {
Ok(data) => {
// Format response
meta.borrow_mut().status = Some(Status::Response);
meta.borrow_mut().description = Some(host);
meta.borrow_mut().title = Some(uri.path());
action_update.activate(None);
// Try create short base for title
let path = uri.path();
let path = Path::new(&path);
if let Some(base) = path.file_name() {
if let Some(base_str) = base.to_str() {
meta.borrow_mut().title = Some(GString::from(base_str));
}
}
// Parse response @TODO read bytes
let parts = Regex::split_simple(
r"^(\d+)?\s([\w]+\/[\w]+)?(.*)?",
&data,
RegexCompileFlags::DEFAULT,
RegexMatchFlags::DEFAULT,
);
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes
match parts.get(1) {
Some(code) => match code.as_str() {
"20" => {
match parts.get(2) {
Some(mime) => match mime.as_str() {
"text/gemini" => {
// Update meta
meta.borrow_mut().mime = Some(Mime::TextGemini);
// Update data
match parts.get(4) {
Some(source) => {
meta.borrow_mut().status = Some(Status::Success);
// This content type may return parsed title
let result = content.reset(content::Mime::TextGemini, &uri, &source);
meta.borrow_mut().title = result.title.clone();
// Add new history record
let request = uri.to_str();
match navigation.history_current() {
Some(current) => {
if current != request {
navigation.history_add(request);
}
}
None => navigation.history_add(request),
}
// Update window components
action_update.activate(None);
},
None => todo!(),
}
},
"text/plain" => {
meta.borrow_mut().status = Some(Status::Success);
meta.borrow_mut().mime = Some(Mime::TextPlain);
action_update.activate(None);
todo!()
},
_ => {
meta.borrow_mut().status = Some(Status::Failure);
meta.borrow_mut().title = Some(gformat!("Oops"));
meta.borrow_mut().description = Some(gformat!("Content {mime} not supported"));
action_update.activate(None);
},
}
None => todo!(),
};
},
// Redirect (@TODO implement limits to auto-redirect)
"31" => {
// Update meta
meta.borrow_mut().status = Some(Status::Redirect);
meta.borrow_mut().mime = Some(Mime::TextGemini);
meta.borrow_mut().title = Some(gformat!("Redirect"));
action_update.activate(None);
// Select widget
match parts.get(3) {
Some(source) => {
let _ = content.reset(
content::Mime::TextGemini,
&uri,
// @TODO use template file
&gformat!("# Redirect\n\nAuto-follow disabled, click on link below to continue\n\n=> {source}")
);
},
None => todo!(),
}
},
// @TODO
_ => {
// Update
meta.borrow_mut().status = Some(Status::Failure);
meta.borrow_mut().title = Some(gformat!("Oops"));
meta.borrow_mut().description = Some(gformat!("Status {code} not supported"));
action_update.activate(None);
},
}
None => todo!(),
};
}
Err(e) => {
meta.borrow_mut().status = Some(Status::Failure);
meta.borrow_mut().title = Some(gformat!("Oops"));
meta.borrow_mut().description = Some(gformat!("Failed to read buffer data: {e}"));
action_update.activate(None);
}
}
// Close connection
if let Err(e) = connection.close(Some(&cancellable)) {
panic!("Error closing connection: {:?}", e);
}
}
Err(e) => {
// Update
meta.borrow_mut().status = Some(Status::Failure);
meta.borrow_mut().title = Some(gformat!("Oops"));
meta.borrow_mut().description = Some(gformat!("Failed to read response: {:?}", e));
action_update.activate(None);
// Close connection
if let Err(e) = connection.close(Some(&cancellable)) {
panic!("Error closing response connection: {:?}", e);
}
}
},
);
}
Err(e) => {
// Update
meta.borrow_mut().status = Some(Status::Failure);
meta.borrow_mut().title = Some(gformat!("Oops"));
meta.borrow_mut().description = Some(gformat!("Failed to read request: {:?}", e));
action_update.activate(None);
// Close connection
if let Err(e) = connection.close(Some(&cancellable)) {
panic!("Error closing request connection: {:?}", e);
}
}
},
);
}
Err(e) => {
// Update
meta.borrow_mut().status = Some(Status::Failure);
meta.borrow_mut().title = Some(gformat!("Oops"));
meta.borrow_mut().description = Some(gformat!("Failed to connect: {:?}", e));
action_update.activate(None);
}
},
);
}
/* @TODO
"nex" => {}
*/
scheme => {
// Update
meta.borrow_mut().status = Some(Status::Failure);
meta.borrow_mut().title = Some(gformat!("Oops"));
meta.borrow_mut().description =
Some(gformat!("Protocol {scheme} not supported"));
action_update.activate(None);
}
}
}
Err(_) => {
// Try interpret URI manually
if Regex::match_simple(
r"^[^\/\s]+\.[\w]{2,}",
request_text.clone(),
RegexCompileFlags::DEFAULT,
RegexMatchFlags::DEFAULT,
) {
// Seems request contain some host, try append default scheme
let request_text = gformat!("gemini://{request_text}");
// Make sure new request conversible to valid URI
match Uri::parse(&request_text, UriFlags::NONE) {
Ok(_) => {
// Update
self.navigation.set_request_text(&request_text);
// Reload page
self.action_tab_page_navigation_reload.activate(None);
}
Err(_) => {
// @TODO any action here?
}
}
} else {
// Plain text given, make search request to default provider
let request_text = gformat!(
"gemini://tlgs.one/search?{}",
Uri::escape_string(&request_text, None, false)
);
// Update
self.navigation.set_request_text(&request_text);
// Reload page
self.action_tab_page_navigation_reload.activate(None);
}
}
};
}
pub fn update(&self) {
// Interpret status to progress fraction
let progress_fraction = match self.meta.borrow().status {
Some(Status::Prepare | Status::Reload) => Some(0.0),
Some(Status::Connect) => Some(0.25),
Some(Status::Request) => Some(0.50),
Some(Status::Response) => Some(0.75),
Some(Status::Failure | Status::Redirect | Status::Success) => Some(1.0),
_ => None,
};
// Update components
self.navigation.update(progress_fraction);
// @TODO self.content.update();
}
// Getters
pub fn title(&self) -> Option<GString> {
self.meta.borrow().title.clone()
}
pub fn description(&self) -> Option<GString> {
self.meta.borrow().description.clone()
}
pub fn widget(&self) -> &Box {
&self.widget
}
}

View file

@ -0,0 +1,68 @@
// @TODO mod image;
mod text;
use text::Text;
use gtk::{
gio::SimpleAction,
glib::{GString, Uri},
prelude::{BoxExt, WidgetExt},
Box, Orientation,
};
use std::sync::Arc;
pub enum Mime {
TextGemini,
// TextPlain,
}
pub struct ResetResult {
pub title: Option<GString>,
}
pub struct Content {
// GTK
widget: Box,
// Actions
action_page_open: Arc<SimpleAction>,
}
impl Content {
// Construct
pub fn new(action_page_open: Arc<SimpleAction>) -> Self {
Self {
widget: Box::builder().orientation(Orientation::Vertical).build(),
action_page_open,
}
}
// Actions
pub fn reset(&self, mime: Mime, base: &Uri, data: &str) -> ResetResult {
// Cleanup
while let Some(child) = self.widget.last_child() {
self.widget.remove(&child)
}
// Re-compose
match mime {
Mime::TextGemini => {
let child = Text::gemini(data, base, self.action_page_open.clone());
self.widget.append(child.widget());
ResetResult {
title: child.meta_title().clone(),
}
} /* @TODO
Mime::TextPlain => {
todo!()
} */
}
}
// Getters
pub fn widget(&self) -> &Box {
&self.widget
}
}

View 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
}
}

View 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
}
}

View file

@ -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
}
}

View file

@ -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;
}

View file

@ -0,0 +1,3 @@
pub mod header;
pub mod link;
pub mod plain;

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -0,0 +1,42 @@
use gtk::glib::GString;
// Page MIME type (not related with gemini status code)
// Useful for content renderer detection, etc
pub enum Mime {
TextGemini,
TextPlain,
}
// Internal page status (not related with gemini status code)
// Useful for widgets composition
pub enum Status {
Connect,
Failure,
Prepare,
Redirect,
Reload,
Request,
Response,
Success,
}
pub struct Meta {
// Text meta data for page
// Useful to update window title, label text, etc
pub title: Option<GString>,
pub description: Option<GString>,
// Enums
pub mime: Option<Mime>,
pub status: Option<Status>,
}
impl Meta {
pub fn new() -> Self {
Self {
title: None,
description: None,
mime: None,
status: None,
}
}
}

View file

@ -0,0 +1,132 @@
mod base;
mod bookmark;
mod history;
mod reload;
mod request;
use base::Base;
use bookmark::Bookmark;
use history::History;
use reload::Reload;
use request::Request;
use gtk::{
gio::SimpleAction,
glib::GString,
prelude::{BoxExt, WidgetExt},
Box, DirectionType, Orientation,
};
use std::sync::Arc;
pub struct Navigation {
// GTK
widget: Box,
// Components
base: Base,
history: History,
reload: Reload,
request: Request,
bookmark: Bookmark,
}
impl Navigation {
pub fn new(
request_text: Option<GString>,
action_tab_page_navigation_base: Arc<SimpleAction>,
action_tab_page_navigation_history_back: Arc<SimpleAction>,
action_tab_page_navigation_history_forward: Arc<SimpleAction>,
action_tab_page_navigation_reload: Arc<SimpleAction>,
action_update: Arc<SimpleAction>,
) -> Self {
// Init components
let base = Base::new(action_tab_page_navigation_base);
let history = History::new(
action_tab_page_navigation_history_back,
action_tab_page_navigation_history_forward,
);
let reload = Reload::new(action_tab_page_navigation_reload.clone());
let request = Request::new(
request_text,
action_update.clone(),
action_tab_page_navigation_reload.clone(),
);
let bookmark = Bookmark::new();
// Init widget
let widget = Box::builder()
.orientation(Orientation::Horizontal)
.spacing(8)
.margin_top(8)
.margin_start(8)
.margin_end(8)
.margin_bottom(8)
.build();
widget.append(base.widget());
widget.append(history.widget());
widget.append(reload.widget());
widget.append(request.widget());
widget.append(bookmark.widget());
// Result
Self {
widget,
base,
history,
reload,
request,
bookmark,
}
}
// Actions
pub fn request_grab_focus(&self) {
self.request.widget().grab_focus();
}
pub fn history_add(&self, request: GString) {
self.history.add(request, true);
}
pub fn history_back(&self, follow_to_index: bool) -> Option<GString> {
self.history.back(follow_to_index)
}
pub fn history_current(&self) -> Option<GString> {
self.history.current()
}
pub fn history_forward(&self, follow_to_index: bool) -> Option<GString> {
self.history.forward(follow_to_index)
}
pub fn update(&self, progress_fraction: Option<f64>) {
self.base.update(self.request.uri());
self.history.update();
self.reload.update(!self.request.is_empty());
self.request.update(progress_fraction);
self.bookmark.update();
}
// Setters
pub fn set_request_text(&self, value: &GString) {
// Focus out from content area on activate the link @TODO
self.widget.child_focus(DirectionType::Right);
self.request.set_text(value);
}
// Getters
pub fn widget(&self) -> &Box {
&self.widget
}
pub fn base_url(&self) -> Option<GString> {
self.base.url()
}
pub fn request_text(&self) -> GString {
self.request.text()
}
}

View file

@ -0,0 +1,79 @@
use gtk::{
gio::SimpleAction,
glib::{gformat, GString, Uri},
prelude::{ActionExt, ButtonExt, WidgetExt},
Button,
};
use std::{cell::RefCell, sync::Arc};
pub struct Base {
// Actions
action_tab_page_navigation_base: Arc<SimpleAction>,
// Mutable URI cache (parsed on update)
uri: RefCell<Option<Uri>>,
// GTK
widget: Button,
}
impl Base {
// Construct
pub fn new(action_tab_page_navigation_base: Arc<SimpleAction>) -> Self {
// Init widget
let widget = Button::builder()
.icon_name("go-home-symbolic")
.tooltip_text("Base")
.sensitive(false)
.build();
// Init events
widget.connect_clicked({
let action_tab_page_navigation_base = action_tab_page_navigation_base.clone();
move |_| {
action_tab_page_navigation_base.activate(None);
}
});
// Return activated struct
Self {
action_tab_page_navigation_base,
uri: RefCell::new(None),
widget,
}
}
// Actions
pub fn update(&self, uri: Option<Uri>) {
// Update sensitivity
let status = match &uri {
Some(uri) => "/" != uri.path(),
None => false,
};
self.action_tab_page_navigation_base.set_enabled(status);
self.widget.set_sensitive(status);
// Update parsed cache
self.uri.replace(uri);
}
// Getters
pub fn widget(&self) -> &Button {
&self.widget
}
pub fn url(&self) -> Option<GString> {
// Build URL from parsed URI cache
if let Some(uri) = self.uri.take() {
let scheme = uri.scheme();
let port = uri.port();
if let Some(host) = uri.host() {
if port.is_positive() {
return Some(gformat!("{scheme}://{host}:{port}/"));
} else {
return Some(gformat!("{scheme}://{host}/"));
} // @TODO auth params
}
}
None
}
}

View file

@ -0,0 +1,28 @@
use gtk::Button;
pub struct Bookmark {
widget: Button,
}
impl Bookmark {
// Construct
pub fn new() -> Self {
Self {
widget: Button::builder()
.icon_name("starred-symbolic")
.tooltip_text("Bookmark")
.sensitive(false)
.build(),
}
}
// Actions
pub fn update(&self) {
// @TODO
}
// Getters
pub fn widget(&self) -> &Button {
&self.widget
}
}

View file

@ -0,0 +1,139 @@
mod back;
mod forward;
use back::Back;
use forward::Forward;
use gtk::{gio::SimpleAction, glib::GString, prelude::BoxExt, Box, Orientation};
use std::{cell::RefCell, sync::Arc};
struct Memory {
request: GString,
// time: SystemTime,
}
pub struct History {
// Components
back: Back,
forward: Forward,
// Extras
memory: RefCell<Vec<Memory>>,
index: RefCell<Option<usize>>,
// GTK
widget: Box,
}
impl History {
// Construct
pub fn new(
action_tab_page_navigation_history_back: Arc<SimpleAction>,
action_tab_page_navigation_history_forward: Arc<SimpleAction>,
) -> Self {
// init components
let back = Back::new(action_tab_page_navigation_history_back);
let forward = Forward::new(action_tab_page_navigation_history_forward);
// Init widget
let widget = Box::builder()
.orientation(Orientation::Horizontal)
.css_classes([
"linked", // merge childs
])
.build();
widget.append(back.widget());
widget.append(forward.widget());
// Init memory
let memory = RefCell::new(Vec::new());
// Init index
let index = RefCell::new(None);
Self {
// Actions
back,
forward,
// Extras
memory,
index,
// GTK
widget,
}
}
// Actions
pub fn add(&self, request: GString, follow_to_index: bool) {
// Append new Memory record
self.memory.borrow_mut().push(Memory {
request: request.clone(),
//time: SystemTime::now(),
});
if follow_to_index {
// Even push action make positive len value, make sure twice
if !self.memory.borrow().is_empty() {
// Navigate to the last record appended
self.index.replace(Some(self.memory.borrow().len() - 1));
} else {
self.index.replace(None);
}
}
}
pub fn back(&self, follow_to_index: bool) -> Option<GString> {
let index = self.index.borrow().clone(); // keep outside as borrow
if let Some(usize) = index {
// Make sure value positive to prevent panic
if usize > 0 {
if let Some(memory) = self.memory.borrow().get(usize - 1) {
if follow_to_index {
self.index.replace(Some(usize - 1));
}
return Some(memory.request.clone());
}
}
}
None
}
pub fn current(&self) -> Option<GString> {
let index = self.index.borrow().clone(); // keep outside as borrow
if let Some(usize) = index {
if let Some(memory) = self.memory.borrow().get(usize) {
return Some(memory.request.clone());
}
}
None
}
pub fn forward(&self, follow_to_index: bool) -> Option<GString> {
let index = self.index.borrow().clone(); // keep outside as borrow
if let Some(usize) = index {
if let Some(memory) = self.memory.borrow().get(usize + 1) {
if follow_to_index {
self.index.replace(Some(usize + 1));
}
return Some(memory.request.clone());
}
}
None
}
pub fn update(&self) {
match self.back(false) {
Some(_) => self.back.update(true),
None => self.back.update(false),
};
match self.forward(false) {
Some(_) => self.forward.update(true),
None => self.forward.update(false),
};
}
// Getters
pub fn widget(&self) -> &Box {
&self.widget
}
}

View file

@ -0,0 +1,50 @@
use gtk::{
gio::SimpleAction,
prelude::{ActionExt, ButtonExt, WidgetExt},
Button,
};
use std::sync::Arc;
pub struct Back {
action_tab_page_navigation_history_back: Arc<SimpleAction>,
widget: Button,
}
impl Back {
// Construct
pub fn new(action_tab_page_navigation_history_back: Arc<SimpleAction>) -> Self {
// Init widget
let widget = Button::builder()
.icon_name("go-previous-symbolic")
.tooltip_text("Back")
.sensitive(false)
.build();
// Init events
widget.connect_clicked({
let action_tab_page_navigation_history_back =
action_tab_page_navigation_history_back.clone();
move |_| {
action_tab_page_navigation_history_back.activate(None);
}
});
// Return activated struct
Self {
action_tab_page_navigation_history_back,
widget,
}
}
// Actions
pub fn update(&self, status: bool) {
self.action_tab_page_navigation_history_back
.set_enabled(status);
self.widget.set_sensitive(status);
}
// Getters
pub fn widget(&self) -> &Button {
&self.widget
}
}

View file

@ -0,0 +1,49 @@
use gtk::{
prelude::{ActionExt, ButtonExt, WidgetExt},
{gio::SimpleAction, Button},
};
use std::sync::Arc;
pub struct Forward {
action_tab_page_navigation_history_forward: Arc<SimpleAction>,
widget: Button,
}
impl Forward {
// Construct
pub fn new(action_tab_page_navigation_history_forward: Arc<SimpleAction>) -> Self {
// Init widget
let widget = Button::builder()
.icon_name("go-next-symbolic")
.tooltip_text("Forward")
.sensitive(false)
.build();
// Init events
widget.connect_clicked({
let action_tab_page_navigation_history_forward =
action_tab_page_navigation_history_forward.clone();
move |_| {
action_tab_page_navigation_history_forward.activate(None);
}
});
// Return activated struct
Self {
action_tab_page_navigation_history_forward,
widget,
}
}
// Actions
pub fn update(&self, status: bool) {
self.action_tab_page_navigation_history_forward
.set_enabled(status);
self.widget.set_sensitive(status);
}
// Getters
pub fn widget(&self) -> &Button {
&self.widget
}
}

View file

@ -0,0 +1,49 @@
use gtk::{
gio::SimpleAction,
prelude::{ActionExt, ButtonExt, WidgetExt},
Button,
};
use std::sync::Arc;
pub struct Reload {
action_tab_page_navigation_reload: Arc<SimpleAction>,
widget: Button,
}
impl Reload {
// Construct
pub fn new(action_tab_page_navigation_reload: Arc<SimpleAction>) -> Self {
// Init widget
let widget = Button::builder()
.icon_name("view-refresh-symbolic")
.tooltip_text("Reload")
.sensitive(false)
.build();
// Init events
widget.connect_clicked({
let action_tab_page_navigation_reload = action_tab_page_navigation_reload.clone();
move |_| {
action_tab_page_navigation_reload.activate(None);
}
});
// Return activated struct
Self {
action_tab_page_navigation_reload,
widget,
}
}
// Actions
pub fn update(&self, is_enabled: bool) {
self.action_tab_page_navigation_reload
.set_enabled(is_enabled);
self.widget.set_sensitive(is_enabled);
}
// Getters
pub fn widget(&self) -> &Button {
&self.widget
}
}

View file

@ -0,0 +1,129 @@
use gtk::{
gio::SimpleAction,
glib::{timeout_add_local, ControlFlow, GString, SourceId, Uri, UriFlags},
prelude::{ActionExt, EditableExt, EntryExt},
Entry,
};
use std::{cell::RefCell, sync::Arc, time::Duration};
// Progressbar animation setup
const PROGRESS_ANIMATION_STEP: f64 = 0.05;
const PROGRESS_ANIMATION_TIME: u64 = 20; //ms
struct Progress {
fraction: RefCell<f64>,
source_id: RefCell<Option<SourceId>>,
}
// Main
pub struct Request {
progress: Arc<Progress>,
widget: Entry,
}
impl Request {
// Construct
pub fn new(
text: Option<GString>,
// Actions
action_update: Arc<SimpleAction>,
action_tab_page_navigation_reload: Arc<SimpleAction>, // @TODO local `action_page_open`?
) -> Self {
// GTK
let widget = Entry::builder()
.placeholder_text("URL or search term...")
.hexpand(true)
.text(match text {
Some(text) => text,
None => GString::new(),
})
.build();
// Connect events
widget.connect_changed(move |_| {
action_update.activate(None);
});
widget.connect_activate(move |_| {
action_tab_page_navigation_reload.activate(None);
});
// Init animated progressbar state
let progress = Arc::new(Progress {
fraction: RefCell::new(0.0),
source_id: RefCell::new(None),
});
// Result
Self { progress, widget }
}
// Actions
pub fn update(&self, progress_fraction: Option<f64>) {
// Skip update animation for Non value
if let Some(value) = progress_fraction {
// Update shared fraction value for async progressbar function, animate on changed only
if value != self.progress.fraction.replace(value) {
// Start new frame on previous process function completed (`source_id` changed to None)
// If previous process still active, we have just updated shared fraction value before, to use it inside the active process
if self.progress.source_id.borrow().is_none() {
// Start new animation frame iterator, update `source_id`
self.progress.source_id.replace(Some(timeout_add_local(
Duration::from_millis(PROGRESS_ANIMATION_TIME),
{
// Clone async pointers dependency
let widget = self.widget.clone();
let progress = self.progress.clone();
// Frame
move || {
// Animate
if *progress.fraction.borrow() > widget.progress_fraction() {
widget.set_progress_fraction(
// Currently, here is no outrange validation, seems that wrapper make this work @TODO
widget.progress_fraction() + PROGRESS_ANIMATION_STEP,
);
return ControlFlow::Continue;
}
// Deactivate
progress.source_id.replace(None);
// Reset (to hide progress widget)
widget.set_progress_fraction(0.0);
// Stop iteration
ControlFlow::Break
}
},
)));
}
}
}
}
// Setters
pub fn set_text(&self, value: &GString) {
self.widget.set_text(value);
}
// Getters
pub fn widget(&self) -> &Entry {
&self.widget
}
pub fn is_empty(&self) -> bool {
0 == self.widget.text_length()
}
pub fn text(&self) -> GString {
self.widget.text()
}
pub fn uri(&self) -> Option<Uri> {
match Uri::parse(&self.widget.text(), UriFlags::NONE) {
Ok(uri) => Some(uri),
_ => None,
}
}
}