diff --git a/Cargo.toml b/Cargo.toml index 119a130f..82e01adb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ features = ["v1_5"] [dependencies.gemini] package = "ggemini" -version = "0.13.0" +version = "0.14.0" [dependencies.gemtext] package = "ggemtext" @@ -47,6 +47,6 @@ syntect = "5.2.0" # development [patch.crates-io] -# ggemini = { git = "https://github.com/YGGverse/ggemini.git" } +ggemini = { git = "https://github.com/YGGverse/ggemini.git" } # ggemtext = { git = "https://github.com/YGGverse/ggemtext.git" } # plurify = { git = "https://github.com/YGGverse/plurify.git" } diff --git a/README.md b/README.md index ee2b2801..0b4718ff 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,9 @@ GTK 4 / Libadwaita client written in Rust * [x] List * [x] Quote * [ ] Gemfeed - * [ ] Titan +* [ ] [Titan](https://transjovian.org/titan/page/The%20Titan%20Specification) + * [ ] Binary data (file uploads) + * [ ] Text input * [ ] [NEX](https://nightfall.city/nex/info/specification.txt) - useful for networks with build-in encryption (e.g. [Yggdrasil](https://yggdrasil-network.github.io)) * [ ] [NPS](https://nightfall.city/nps/info/specification.txt) * [ ] Localhost @@ -157,7 +159,7 @@ GTK 4 / Libadwaita client written in Rust ``` bash sudo apt install git curl build-essential\ - libgtk-4-dev libgtksourceview-5-dev libadwaita-1-dev libspelling-1-dev\ + libgtk-4-dev libgtksourceview-5-dev libglib2.0-dev libadwaita-1-dev libspelling-1-dev\ libsqlite3-dev libssl-dev ``` @@ -165,7 +167,7 @@ sudo apt install git curl build-essential\ ``` bash sudo dnf install git curl gcc\ - gtk4-devel gtksourceview5-devel libadwaita-devel libspelling-devel\ + gtk4-devel gtksourceview5-devel glib2-devel libadwaita-devel libspelling-devel\ sqlite-devel openssl-devel ``` diff --git a/src/app/browser/window/tab/item/page.rs b/src/app/browser/window/tab/item/page.rs index 26c572f0..14b72352 100644 --- a/src/app/browser/window/tab/item/page.rs +++ b/src/app/browser/window/tab/item/page.rs @@ -231,6 +231,24 @@ impl Page { }; self.load_gemini(uri, is_download, is_source, is_history) } + "titan" => { + // Format response + let status = Status::Input; + let title = gformat!("Titan input"); + + // Toggle input form + self.input.set_new_titan( + self.tab_action.clone(), + uri.clone(), + Some(&title), + ); + + // Update meta + self.meta.set_status(status).set_title(&title); + + // Update page + self.browser_action.update.activate(Some(&self.id)); + } scheme => { // Add history record if is_history { @@ -385,7 +403,7 @@ impl Page { // @TODO move outside fn load_gemini(&self, uri: Uri, is_download: bool, is_source: bool, is_history: bool) { // Init local namespace - use gemini::client::connection::response; + use gemini::client::connection::{response, Request}; // Init shared clones let browser_action = self.browser_action.clone(); @@ -425,7 +443,10 @@ impl Page { // Begin new socket request self.client.gemini.request_async( - uri.clone(), + match uri.scheme().as_str() { + "titan" => Request::titan(uri.clone(), Vec::new(), None, None), // @TODO + _ => Request::gemini(uri.clone()) + }, Priority::DEFAULT, cancellable.clone(), // Search for user certificate match request @@ -512,7 +533,7 @@ impl Page { move |_, total| action.update.activate( &format!( "Received {}...", - format_bytes(total) + crate::tool::format_bytes(total) ) ) }, @@ -945,16 +966,3 @@ fn snap_history(profile: &Profile, navigation: &Navigation, uri: Option<&Uri>) { navigation.history.add(request, true) } } - -/// Format bytes to KB/MB/GB presentation -fn format_bytes(value: usize) -> String { - if value < 1024 { - format!("{} bytes", value) - } else if value < 1024 * 1024 { - format!("{:.2} KB", value as f64 / 1024.0) - } else if value < 1024 * 1024 * 1024 { - format!("{:.2} MB", value as f64 / (1024.0 * 1024.0)) - } else { - format!("{:.2} GB", value as f64 / (1024.0 * 1024.0 * 1024.0)) - } -} diff --git a/src/app/browser/window/tab/item/page/content/text/gemini/reader.rs b/src/app/browser/window/tab/item/page/content/text/gemini/reader.rs index c7bcf77d..9da102c1 100644 --- a/src/app/browser/window/tab/item/page/content/text/gemini/reader.rs +++ b/src/app/browser/window/tab/item/page/content/text/gemini/reader.rs @@ -330,7 +330,7 @@ impl Reader { if let Some(uri) = links.get(&tag) { // Select link handler by scheme return match uri.scheme().as_str() { - "gemini" => { + "gemini" | "titan" => { // Open new page in browser tab_action.load.activate(Some(&uri.to_str()), true); } @@ -367,7 +367,7 @@ impl Reader { if let Some(uri) = links.get(&tag) { // Select link handler by scheme return match uri.scheme().as_str() { - "gemini" => { + "gemini" | "titan" => { // Open new page in browser window_action.append.activate_stateful_once( Position::After, diff --git a/src/app/browser/window/tab/item/page/input.rs b/src/app/browser/window/tab/item/page/input.rs index 2241c57a..e82982a9 100644 --- a/src/app/browser/window/tab/item/page/input.rs +++ b/src/app/browser/window/tab/item/page/input.rs @@ -1,9 +1,11 @@ mod response; mod sensitive; +mod titan; mod widget; use response::Response; use sensitive::Sensitive; +use titan::Titan; use widget::Widget; use crate::app::browser::window::tab::item::Action as TabAction; @@ -44,7 +46,9 @@ impl Input { size_limit: Option, ) { self.widget.update(Some( - &Response::new(action, base, title, size_limit).widget.g_box, + &Response::build(action, base, title, size_limit) + .widget + .g_box, )); } @@ -56,7 +60,14 @@ impl Input { max_length: Option, ) { self.widget.update(Some( - Sensitive::new(action, base, title, max_length).gobject(), + &Sensitive::build(action, base, title, max_length) + .widget + .g_box, )); } + + pub fn set_new_titan(&self, action: Rc, base: Uri, title: Option<&str>) { + self.widget + .update(Some(&Titan::build(action, base, title).widget.g_box)); + } } diff --git a/src/app/browser/window/tab/item/page/input/response.rs b/src/app/browser/window/tab/item/page/input/response.rs index 109ba12e..079294be 100644 --- a/src/app/browser/window/tab/item/page/input/response.rs +++ b/src/app/browser/window/tab/item/page/input/response.rs @@ -8,11 +8,10 @@ use form::Form; use title::Title; use widget::Widget; -use crate::app::browser::window::tab::item::action::Action as TabAction; +use super::TabAction; use gtk::{ gio::SimpleAction, glib::{uuid_string_random, Uri, UriHideFlags}, - prelude::WidgetExt, }; use std::rc::Rc; @@ -22,8 +21,10 @@ pub struct Response { } impl Response { - // Construct - pub fn new( + // Constructors + + /// Build new `Self` + pub fn build( tab_action: Rc, base: Uri, title: Option<&str>, @@ -34,12 +35,12 @@ impl Response { let action_send = SimpleAction::new(&uuid_string_random(), None); // Init components - let control = Rc::new(Control::new(action_send.clone())); - let form = Rc::new(Form::new(action_update.clone())); - let title = Rc::new(Title::new(title)); + let control = Rc::new(Control::build(action_send.clone())); + let form = Rc::new(Form::build(action_update.clone())); + let title = Rc::new(Title::build(title)); // Init widget - let widget = Rc::new(Widget::new( + let widget = Rc::new(Widget::build( &title.widget.label, &form.widget.text_view, &control.widget.g_box, @@ -52,10 +53,10 @@ impl Response { let form = form.clone(); move |_, _| { control.update(size_limit.map(|limit| { - limit as i32 - - (base.to_string_partial(UriHideFlags::QUERY).len() as i32 - + Uri::escape_string(&form.widget.text(), None, false).len() as i32) - })); + limit + - (base.to_string_partial(UriHideFlags::QUERY).len() + + Uri::escape_string(&form.widget.text(), None, false).len()) + })) } }); @@ -73,10 +74,6 @@ impl Response { } }); - widget.g_box.connect_realize(move |_| { - form.widget.text_view.grab_focus(); - }); - // Return activated struct Self { widget } } diff --git a/src/app/browser/window/tab/item/page/input/response/control.rs b/src/app/browser/window/tab/item/page/input/response/control.rs index 6919c183..47e94d8b 100644 --- a/src/app/browser/window/tab/item/page/input/response/control.rs +++ b/src/app/browser/window/tab/item/page/input/response/control.rs @@ -16,14 +16,16 @@ pub struct Control { } impl Control { - // Construct - pub fn new(action_send: SimpleAction) -> Self { + // Constructors + + /// Build new `Self` + pub fn build(action_send: SimpleAction) -> Self { // Init components let counter = Rc::new(Counter::new()); - let send = Rc::new(Send::new(action_send)); + let send = Rc::new(Send::build(action_send)); // Init widget - let widget = Rc::new(Widget::new(&counter.widget.label, &send.widget.button)); + let widget = Rc::new(Widget::build(&counter.widget.label, &send.widget.button)); // Return activated struct Self { @@ -34,10 +36,10 @@ impl Control { } // Actions - pub fn update(&self, chars_left: Option) { + pub fn update(&self, bytes_left: Option) { // Update children components - self.counter.update(chars_left); - self.send.update(match chars_left { + self.counter.update(bytes_left); + self.send.update(match bytes_left { Some(left) => left > 0, None => false, }); diff --git a/src/app/browser/window/tab/item/page/input/response/control/counter.rs b/src/app/browser/window/tab/item/page/input/response/control/counter.rs index c3e8e956..f7adf8b2 100644 --- a/src/app/browser/window/tab/item/page/input/response/control/counter.rs +++ b/src/app/browser/window/tab/item/page/input/response/control/counter.rs @@ -23,7 +23,7 @@ impl Counter { } // Actions - pub fn update(&self, chars_left: Option) { - self.widget.update(chars_left); + pub fn update(&self, bytes_left: Option) { + self.widget.update(bytes_left); } } diff --git a/src/app/browser/window/tab/item/page/input/response/control/counter/widget.rs b/src/app/browser/window/tab/item/page/input/response/control/counter/widget.rs index 84766f59..2020dd35 100644 --- a/src/app/browser/window/tab/item/page/input/response/control/counter/widget.rs +++ b/src/app/browser/window/tab/item/page/input/response/control/counter/widget.rs @@ -19,8 +19,8 @@ impl Widget { } // Actions - pub fn update(&self, chars_left: Option) { - match chars_left { + pub fn update(&self, bytes_left: Option) { + match bytes_left { Some(value) => { // Update color on chars left reached self.label diff --git a/src/app/browser/window/tab/item/page/input/response/control/send.rs b/src/app/browser/window/tab/item/page/input/response/control/send.rs index 95c99577..299b298c 100644 --- a/src/app/browser/window/tab/item/page/input/response/control/send.rs +++ b/src/app/browser/window/tab/item/page/input/response/control/send.rs @@ -9,10 +9,12 @@ pub struct Send { } impl Send { - // Construct - pub fn new(action_send: SimpleAction) -> Self { + // Constructors + + /// Build new `Self` + pub fn build(action_send: SimpleAction) -> Self { // Init widget - let widget = Rc::new(Widget::new(action_send)); + let widget = Rc::new(Widget::build(action_send)); // Result Self { widget } diff --git a/src/app/browser/window/tab/item/page/input/response/control/send/widget.rs b/src/app/browser/window/tab/item/page/input/response/control/send/widget.rs index 326a28fe..bd0bf6e7 100644 --- a/src/app/browser/window/tab/item/page/input/response/control/send/widget.rs +++ b/src/app/browser/window/tab/item/page/input/response/control/send/widget.rs @@ -9,12 +9,15 @@ pub struct Widget { } impl Widget { - // Construct - pub fn new(action_send: SimpleAction) -> Self { + // Constructors + + /// Build new `Self` + pub fn build(action_send: SimpleAction) -> Self { // Init main widget let button = Button::builder() .css_classes(["accent"]) // | `suggested-action` .label("Send") + .sensitive(false) .build(); // Init events diff --git a/src/app/browser/window/tab/item/page/input/response/control/widget.rs b/src/app/browser/window/tab/item/page/input/response/control/widget.rs index 427205c2..7242866e 100644 --- a/src/app/browser/window/tab/item/page/input/response/control/widget.rs +++ b/src/app/browser/window/tab/item/page/input/response/control/widget.rs @@ -7,8 +7,10 @@ pub struct Widget { } impl Widget { - // Construct - pub fn new(limit: &Label, send: &Button) -> Self { + // Constructors + + /// Build new `Self` + pub fn build(limit: &Label, send: &Button) -> Self { // Init main widget let g_box = Box::builder() .halign(Align::End) diff --git a/src/app/browser/window/tab/item/page/input/response/form.rs b/src/app/browser/window/tab/item/page/input/response/form.rs index f75a2ad7..1fbc0bbd 100644 --- a/src/app/browser/window/tab/item/page/input/response/form.rs +++ b/src/app/browser/window/tab/item/page/input/response/form.rs @@ -10,10 +10,12 @@ pub struct Form { } impl Form { - // Construct - pub fn new(action_update: SimpleAction) -> Self { + // Constructors + + /// Build new `Self` + pub fn build(action_update: SimpleAction) -> Self { Self { - widget: Rc::new(Widget::new(action_update)), + widget: Rc::new(Widget::build(action_update)), } } } diff --git a/src/app/browser/window/tab/item/page/input/response/form/widget.rs b/src/app/browser/window/tab/item/page/input/response/form/widget.rs index 380a3bb2..be21415e 100644 --- a/src/app/browser/window/tab/item/page/input/response/form/widget.rs +++ b/src/app/browser/window/tab/item/page/input/response/form/widget.rs @@ -14,8 +14,10 @@ pub struct Widget { } impl Widget { - // Construct - pub fn new(action_update: SimpleAction) -> Self { + // Constructors + + /// Build new `Self` + pub fn build(action_update: SimpleAction) -> Self { // Init [SourceView](https://gitlab.gnome.org/GNOME/gtksourceview) type buffer let buffer = Buffer::builder().build(); @@ -44,6 +46,10 @@ impl Widget { action_update.activate(None); }); + text_view.connect_realize(move |this| { + this.grab_focus(); + }); + // Return activated `Self` Self { text_view } } diff --git a/src/app/browser/window/tab/item/page/input/response/title.rs b/src/app/browser/window/tab/item/page/input/response/title.rs index de5d2ff1..383e6875 100644 --- a/src/app/browser/window/tab/item/page/input/response/title.rs +++ b/src/app/browser/window/tab/item/page/input/response/title.rs @@ -9,10 +9,12 @@ pub struct Title { } impl Title { - // Construct - pub fn new(title: Option<&str>) -> Self { + // Constructors + + /// Build new `Self` + pub fn build(title: Option<&str>) -> Self { Self { - widget: Rc::new(Widget::new(title)), + widget: Rc::new(Widget::build(title)), } } } diff --git a/src/app/browser/window/tab/item/page/input/response/title/widget.rs b/src/app/browser/window/tab/item/page/input/response/title/widget.rs index 2bcb3c5a..107ecce9 100644 --- a/src/app/browser/window/tab/item/page/input/response/title/widget.rs +++ b/src/app/browser/window/tab/item/page/input/response/title/widget.rs @@ -5,8 +5,10 @@ pub struct Widget { } impl Widget { - // Construct - pub fn new(title: Option<&str>) -> Self { + // Constructors + + /// Build new `Self` + pub fn build(title: Option<&str>) -> Self { let label = Label::builder() .css_classes(["heading"]) .halign(Align::Start) diff --git a/src/app/browser/window/tab/item/page/input/response/widget.rs b/src/app/browser/window/tab/item/page/input/response/widget.rs index 54d4739c..857b6cb4 100644 --- a/src/app/browser/window/tab/item/page/input/response/widget.rs +++ b/src/app/browser/window/tab/item/page/input/response/widget.rs @@ -8,8 +8,10 @@ pub struct Widget { } impl Widget { - // Construct - pub fn new(title: &Label, response: &TextView, control: &Box) -> Self { + // Constructors + + /// Build new `Self` + pub fn build(title: &Label, response: &TextView, control: &Box) -> Self { let g_box = Box::builder() .margin_bottom(MARGIN) .margin_end(MARGIN) diff --git a/src/app/browser/window/tab/item/page/input/sensitive.rs b/src/app/browser/window/tab/item/page/input/sensitive.rs index 99428c11..124541bd 100644 --- a/src/app/browser/window/tab/item/page/input/sensitive.rs +++ b/src/app/browser/window/tab/item/page/input/sensitive.rs @@ -4,23 +4,23 @@ mod widget; use form::Form; use widget::Widget; -use crate::app::browser::window::tab::item::action::Action as TabAction; +use super::TabAction; use gtk::{ gio::SimpleAction, glib::{uuid_string_random, Uri, UriHideFlags}, prelude::{EditableExt, WidgetExt}, - Box, }; use std::rc::Rc; pub struct Sensitive { - // Components - widget: Rc, + pub widget: Rc, } impl Sensitive { - // Construct - pub fn new( + // Constructors + + /// Build new `Self` + pub fn build( tab_action: Rc, base: Uri, title: Option<&str>, @@ -30,7 +30,7 @@ impl Sensitive { let action_send = SimpleAction::new(&uuid_string_random(), None); // Init components - let form = Rc::new(Form::new( + let form = Rc::new(Form::build( action_send.clone(), title, max_length @@ -38,7 +38,7 @@ impl Sensitive { )); // Init widget - let widget = Rc::new(Widget::new(&form.widget.password_entry_row)); + let widget = Rc::new(Widget::build(&form.widget.password_entry_row)); // Init events action_send.connect_activate({ @@ -55,16 +55,11 @@ impl Sensitive { } }); - widget.gobject().connect_realize(move |_| { + widget.g_box.connect_realize(move |_| { form.widget.password_entry_row.grab_focus(); }); // Return activated struct Self { widget } } - - // Getters - pub fn gobject(&self) -> &Box { - self.widget.gobject() - } } diff --git a/src/app/browser/window/tab/item/page/input/sensitive/form.rs b/src/app/browser/window/tab/item/page/input/sensitive/form.rs index a9532c36..217314cc 100644 --- a/src/app/browser/window/tab/item/page/input/sensitive/form.rs +++ b/src/app/browser/window/tab/item/page/input/sensitive/form.rs @@ -10,10 +10,12 @@ pub struct Form { } impl Form { - // Construct - pub fn new(action_send: SimpleAction, title: Option<&str>, max_length: Option) -> Self { + // Constructors + + /// Build new `Self` + pub fn build(action_send: SimpleAction, title: Option<&str>, max_length: Option) -> Self { // Init widget - let widget = Rc::new(Widget::new(action_send, title, max_length)); + let widget = Rc::new(Widget::build(action_send, title, max_length)); // Result Self { widget } diff --git a/src/app/browser/window/tab/item/page/input/sensitive/form/widget.rs b/src/app/browser/window/tab/item/page/input/sensitive/form/widget.rs index 2e023d79..be74d0b1 100644 --- a/src/app/browser/window/tab/item/page/input/sensitive/form/widget.rs +++ b/src/app/browser/window/tab/item/page/input/sensitive/form/widget.rs @@ -2,15 +2,20 @@ use adw::{ prelude::{EntryRowExt, PreferencesRowExt}, PasswordEntryRow, }; -use gtk::{gio::SimpleAction, prelude::ActionExt}; +use gtk::{ + gio::SimpleAction, + prelude::{ActionExt, WidgetExt}, +}; pub struct Widget { pub password_entry_row: PasswordEntryRow, } impl Widget { - // Construct - pub fn new(action_send: SimpleAction, title: Option<&str>, _max_length: Option) -> Self { + // Constructors + + /// Build new `Self` + pub fn build(action_send: SimpleAction, title: Option<&str>, _max_length: Option) -> Self { // Init main widget let password_entry_row = PasswordEntryRow::builder().show_apply_button(true).build(); @@ -28,6 +33,10 @@ impl Widget { action_send.activate(None); }); + password_entry_row.connect_realize(move |this| { + this.grab_focus(); + }); + // Return activated struct Self { password_entry_row } } diff --git a/src/app/browser/window/tab/item/page/input/sensitive/widget.rs b/src/app/browser/window/tab/item/page/input/sensitive/widget.rs index 0629fd1d..1d2cbcec 100644 --- a/src/app/browser/window/tab/item/page/input/sensitive/widget.rs +++ b/src/app/browser/window/tab/item/page/input/sensitive/widget.rs @@ -5,13 +5,15 @@ const MARGIN: i32 = 6; const SPACING: i32 = 8; pub struct Widget { - gobject: Box, + pub g_box: Box, } impl Widget { - // Construct - pub fn new(response: &PasswordEntryRow) -> Self { - let gobject = Box::builder() + // Constructors + + /// Build new `Self` + pub fn build(response: &PasswordEntryRow) -> Self { + let g_box = Box::builder() .margin_bottom(MARGIN) .margin_end(MARGIN) .margin_start(MARGIN) @@ -20,13 +22,8 @@ impl Widget { .orientation(Orientation::Vertical) .build(); - gobject.append(response); + g_box.append(response); - Self { gobject } - } - - // Getters - pub fn gobject(&self) -> &Box { - &self.gobject + Self { g_box } } } diff --git a/src/app/browser/window/tab/item/page/input/titan.rs b/src/app/browser/window/tab/item/page/input/titan.rs new file mode 100644 index 00000000..0f252d2a --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan.rs @@ -0,0 +1,65 @@ +mod control; +mod form; +mod title; +mod widget; + +use control::Control; +use form::Form; +use title::Title; +use widget::Widget; + +use super::TabAction; +use gtk::{ + gio::SimpleAction, + glib::{uuid_string_random, Uri}, +}; +use std::rc::Rc; + +pub struct Titan { + // Components + pub widget: Rc, +} + +impl Titan { + // Constructors + + /// Build new `Self` + pub fn build(_tab_action: Rc, _base: Uri, title: Option<&str>) -> Self { + // Init local actions + let action_update = SimpleAction::new(&uuid_string_random(), None); + let action_send = SimpleAction::new(&uuid_string_random(), None); + + // Init components + let control = Rc::new(Control::build(action_send.clone())); + let form = Rc::new(Form::build(action_update.clone())); + let title = Rc::new(Title::build(title)); + + // Init widget + let widget = Rc::new(Widget::build( + &title.widget.label, + &form.widget.text_view, + &control.widget.g_box, + )); + + // Init events + action_update.connect_activate({ + let control = control.clone(); + let form = form.clone(); + move |_, _| control.update(Some(form.widget.size())) + }); + + action_send.connect_activate({ + // @TODO let form = form.clone(); + move |_, _| { + todo!() + /* tab_action.load.activate( + Some(&), + true, + );*/ + } + }); + + // Return activated struct + Self { widget } + } +} diff --git a/src/app/browser/window/tab/item/page/input/titan/control.rs b/src/app/browser/window/tab/item/page/input/titan/control.rs new file mode 100644 index 00000000..87151085 --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan/control.rs @@ -0,0 +1,47 @@ +mod counter; +mod send; +mod widget; + +use counter::Counter; +use send::Send; +use widget::Widget; + +use gtk::gio::SimpleAction; +use std::rc::Rc; + +pub struct Control { + pub counter: Rc, + pub send: Rc, + pub widget: Rc, +} + +impl Control { + // Constructors + + /// Build new `Self` + pub fn build(action_send: SimpleAction) -> Self { + // Init components + let counter = Rc::new(Counter::new()); + let send = Rc::new(Send::build(action_send)); + + // Init widget + let widget = Rc::new(Widget::build(&counter.widget.label, &send.widget.button)); + + // Return activated struct + Self { + counter, + send, + widget, + } + } + + // Actions + pub fn update(&self, bytes: Option) { + // Update children components + self.counter.update(bytes); + self.send.update(match bytes { + Some(left) => left > 0, + None => false, + }); + } +} diff --git a/src/app/browser/window/tab/item/page/input/titan/control/counter.rs b/src/app/browser/window/tab/item/page/input/titan/control/counter.rs new file mode 100644 index 00000000..6347eadd --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan/control/counter.rs @@ -0,0 +1,29 @@ +mod widget; + +use widget::Widget; + +use std::rc::Rc; + +pub struct Counter { + pub widget: Rc, +} + +impl Default for Counter { + fn default() -> Self { + Self::new() + } +} + +impl Counter { + // Construct + pub fn new() -> Self { + Self { + widget: Rc::new(Widget::new()), + } + } + + // Actions + pub fn update(&self, bytes: Option) { + self.widget.update(bytes); + } +} diff --git a/src/app/browser/window/tab/item/page/input/titan/control/counter/widget.rs b/src/app/browser/window/tab/item/page/input/titan/control/counter/widget.rs new file mode 100644 index 00000000..2c754acf --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan/control/counter/widget.rs @@ -0,0 +1,31 @@ +use gtk::{prelude::WidgetExt, Label}; + +pub struct Widget { + pub label: Label, +} + +impl Default for Widget { + fn default() -> Self { + Self::new() + } +} + +impl Widget { + // Construct + pub fn new() -> Self { + Self { + label: Label::builder().css_classes(["dim-label"]).build(), // @TODO `.dimmed` Since: Adw 1.7 + } + } + + // Actions + pub fn update(&self, bytes: Option) { + match bytes { + Some(value) => { + self.label.set_label(&crate::tool::format_bytes(value)); + self.label.set_visible(value > 0); + } + None => self.label.set_visible(false), + } + } +} diff --git a/src/app/browser/window/tab/item/page/input/titan/control/send.rs b/src/app/browser/window/tab/item/page/input/titan/control/send.rs new file mode 100644 index 00000000..299b298c --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan/control/send.rs @@ -0,0 +1,27 @@ +mod widget; +use widget::Widget; + +use gtk::gio::SimpleAction; +use std::rc::Rc; + +pub struct Send { + pub widget: Rc, +} + +impl Send { + // Constructors + + /// Build new `Self` + pub fn build(action_send: SimpleAction) -> Self { + // Init widget + let widget = Rc::new(Widget::build(action_send)); + + // Result + Self { widget } + } + + // Actions + pub fn update(&self, is_sensitive: bool) { + self.widget.update(is_sensitive); + } +} diff --git a/src/app/browser/window/tab/item/page/input/titan/control/send/widget.rs b/src/app/browser/window/tab/item/page/input/titan/control/send/widget.rs new file mode 100644 index 00000000..bd0bf6e7 --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan/control/send/widget.rs @@ -0,0 +1,38 @@ +use gtk::{ + gio::SimpleAction, + prelude::{ActionExt, ButtonExt, WidgetExt}, + Button, +}; + +pub struct Widget { + pub button: Button, +} + +impl Widget { + // Constructors + + /// Build new `Self` + pub fn build(action_send: SimpleAction) -> Self { + // Init main widget + let button = Button::builder() + .css_classes(["accent"]) // | `suggested-action` + .label("Send") + .sensitive(false) + .build(); + + // Init events + button.connect_clicked({ + move |_| { + action_send.activate(None); + } + }); + + // Return activated `Self` + Self { button } + } + + // Actions + pub fn update(&self, is_sensitive: bool) { + self.button.set_sensitive(is_sensitive); + } +} diff --git a/src/app/browser/window/tab/item/page/input/titan/control/widget.rs b/src/app/browser/window/tab/item/page/input/titan/control/widget.rs new file mode 100644 index 00000000..7242866e --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan/control/widget.rs @@ -0,0 +1,27 @@ +use gtk::{prelude::BoxExt, Align, Box, Button, Label, Orientation}; + +const SPACING: i32 = 8; + +pub struct Widget { + pub g_box: Box, +} + +impl Widget { + // Constructors + + /// Build new `Self` + pub fn build(limit: &Label, send: &Button) -> Self { + // Init main widget + let g_box = Box::builder() + .halign(Align::End) + .orientation(Orientation::Horizontal) + .spacing(SPACING) + .build(); + + g_box.append(limit); + g_box.append(send); + + // Return new `Self` + Self { g_box } + } +} diff --git a/src/app/browser/window/tab/item/page/input/titan/form.rs b/src/app/browser/window/tab/item/page/input/titan/form.rs new file mode 100644 index 00000000..e0bcdb3a --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan/form.rs @@ -0,0 +1,21 @@ +mod widget; + +use widget::Widget; + +use gtk::gio::SimpleAction; +use std::rc::Rc; + +pub struct Form { + pub widget: Rc, +} + +impl Form { + // Constructors + + /// Build new `Self` + pub fn build(action_update: SimpleAction) -> Self { + Self { + widget: Rc::new(Widget::new(action_update)), + } + } +} diff --git a/src/app/browser/window/tab/item/page/input/titan/form/widget.rs b/src/app/browser/window/tab/item/page/input/titan/form/widget.rs new file mode 100644 index 00000000..840f93b9 --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan/form/widget.rs @@ -0,0 +1,62 @@ +use gtk::{ + gio::SimpleAction, + prelude::{ActionExt, TextBufferExt, TextViewExt, WidgetExt}, + TextView, WrapMode, +}; +use libspelling::{Checker, TextBufferAdapter}; +use sourceview::Buffer; + +const MARGIN: i32 = 8; + +pub struct Widget { + pub text_view: TextView, +} + +impl Widget { + // Construct + pub fn new(action_update: SimpleAction) -> Self { + // Init [SourceView](https://gitlab.gnome.org/GNOME/gtksourceview) type buffer + let buffer = Buffer::builder().build(); + + // Init [libspelling](https://gitlab.gnome.org/GNOME/libspelling) + let checker = Checker::default(); + let adapter = TextBufferAdapter::new(&buffer, &checker); + adapter.set_enabled(true); + + // Init main widget + let text_view = TextView::builder() + .bottom_margin(MARGIN) + .buffer(&buffer) + .css_classes(["frame", "view"]) + .extra_menu(&adapter.menu_model()) + .left_margin(MARGIN) + .margin_bottom(MARGIN / 4) + .right_margin(MARGIN) + .top_margin(MARGIN) + .wrap_mode(WrapMode::Word) + .build(); + + text_view.insert_action_group("spelling", Some(&adapter)); + + // Init events + text_view.buffer().connect_changed(move |_| { + action_update.activate(None); + }); + + text_view.connect_realize(move |this| { + this.grab_focus(); + }); + + // Return activated `Self` + Self { text_view } + } + + // Getters + + pub fn size(&self) -> usize { + let buffer = self.text_view.buffer(); + buffer + .text(&buffer.start_iter(), &buffer.end_iter(), true) + .len() + } +} diff --git a/src/app/browser/window/tab/item/page/input/titan/title.rs b/src/app/browser/window/tab/item/page/input/titan/title.rs new file mode 100644 index 00000000..383e6875 --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan/title.rs @@ -0,0 +1,20 @@ +mod widget; + +use widget::Widget; + +use std::rc::Rc; + +pub struct Title { + pub widget: Rc, +} + +impl Title { + // Constructors + + /// Build new `Self` + pub fn build(title: Option<&str>) -> Self { + Self { + widget: Rc::new(Widget::build(title)), + } + } +} diff --git a/src/app/browser/window/tab/item/page/input/titan/title/widget.rs b/src/app/browser/window/tab/item/page/input/titan/title/widget.rs new file mode 100644 index 00000000..107ecce9 --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan/title/widget.rs @@ -0,0 +1,27 @@ +use gtk::{prelude::WidgetExt, Align, Label}; + +pub struct Widget { + pub label: Label, +} + +impl Widget { + // Constructors + + /// Build new `Self` + pub fn build(title: Option<&str>) -> Self { + let label = Label::builder() + .css_classes(["heading"]) + .halign(Align::Start) + .visible(false) + .build(); + + if let Some(value) = title { + if !value.is_empty() { + label.set_label(value); + label.set_visible(true) + } + } + + Self { label } + } +} diff --git a/src/app/browser/window/tab/item/page/input/titan/widget.rs b/src/app/browser/window/tab/item/page/input/titan/widget.rs new file mode 100644 index 00000000..857b6cb4 --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan/widget.rs @@ -0,0 +1,30 @@ +use gtk::{prelude::BoxExt, Box, Label, Orientation, TextView}; + +const MARGIN: i32 = 6; +const SPACING: i32 = 8; + +pub struct Widget { + pub g_box: Box, +} + +impl Widget { + // Constructors + + /// Build new `Self` + pub fn build(title: &Label, response: &TextView, control: &Box) -> Self { + let g_box = Box::builder() + .margin_bottom(MARGIN) + .margin_end(MARGIN) + .margin_start(MARGIN) + .margin_top(MARGIN) + .spacing(SPACING) + .orientation(Orientation::Vertical) + .build(); + + g_box.append(title); + g_box.append(response); + g_box.append(control); + + Self { g_box } + } +} diff --git a/src/app/browser/window/tab/item/page/navigation/request/widget.rs b/src/app/browser/window/tab/item/page/navigation/request/widget.rs index e717506a..24fcb9fa 100644 --- a/src/app/browser/window/tab/item/page/navigation/request/widget.rs +++ b/src/app/browser/window/tab/item/page/navigation/request/widget.rs @@ -217,6 +217,10 @@ impl Widget { self.entry.set_primary_icon_name(Some(name)); self.entry.set_primary_icon_tooltip_text(Some(tooltip)); } + PrimaryIcon::Titan { name, tooltip } => { + self.entry.set_primary_icon_name(Some(name)); + self.entry.set_primary_icon_tooltip_text(Some(tooltip)); + } } // Update progress diff --git a/src/app/browser/window/tab/item/page/navigation/request/widget/primary_icon.rs b/src/app/browser/window/tab/item/page/navigation/request/widget/primary_icon.rs index 5090e1a9..14754bcb 100644 --- a/src/app/browser/window/tab/item/page/navigation/request/widget/primary_icon.rs +++ b/src/app/browser/window/tab/item/page/navigation/request/widget/primary_icon.rs @@ -15,6 +15,10 @@ pub enum PrimaryIcon<'a> { name: &'a str, tooltip: &'a str, }, + Titan { + name: &'a str, + tooltip: &'a str, + }, } pub fn from(request: &str) -> PrimaryIcon { @@ -39,6 +43,13 @@ pub fn from(request: &str) -> PrimaryIcon { }; } + if request.starts_with("titan:") { + return PrimaryIcon::Titan { + name: "document-send-symbolic", + tooltip: "Titan input", + }; + } + PrimaryIcon::Search { name: "system-search-symbolic", tooltip: "Search", diff --git a/src/main.rs b/src/main.rs index c3c56a37..c3adb2fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod app; mod profile; +mod tool; use app::App; use profile::Profile; diff --git a/src/tool.rs b/src/tool.rs new file mode 100644 index 00000000..b1ec934b --- /dev/null +++ b/src/tool.rs @@ -0,0 +1,23 @@ +//! Some shared helpers collection + +/// Format bytes to KB/MB/GB presentation +pub fn format_bytes(value: usize) -> String { + const KB: f32 = 1024.0; + const MB: f32 = KB * KB; + const GB: f32 = MB * KB; + + let f = value as f32; + + if f < KB { + format!( + "{value} {}", + plurify::ns(value, &["byte", "bytes", "bytes"]) + ) + } else if f < MB { + format!("{:.2} KB", f / KB) + } else if f < GB { + format!("{:.2} MB", f / MB) + } else { + format!("{:.2} GB", f / GB) + } +}