From afc33c1a038c5ace1a5a8225f9e552d8b20899d6 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 31 Jan 2025 20:06:52 +0200 Subject: [PATCH] implement resend form ability on request failure --- .../window/tab/item/client/driver/gemini.rs | 631 +++++++++--------- src/app/browser/window/tab/item/page/input.rs | 8 +- .../window/tab/item/page/input/titan.rs | 63 +- .../tab/item/page/input/titan/control.rs | 29 +- .../item/page/input/titan/control/counter.rs | 31 +- .../tab/item/page/input/titan/control/send.rs | 44 +- .../window/tab/item/page/input/titan/form.rs | 22 +- .../window/tab/item/page/input/titan/title.rs | 23 +- 8 files changed, 406 insertions(+), 445 deletions(-) diff --git a/src/app/browser/window/tab/item/client/driver/gemini.rs b/src/app/browser/window/tab/item/client/driver/gemini.rs index f0b3e06a..ec43d837 100644 --- a/src/app/browser/window/tab/item/client/driver/gemini.rs +++ b/src/app/browser/window/tab/item/client/driver/gemini.rs @@ -73,13 +73,14 @@ impl Gemini { self.redirects.clone(), feature, cancellable, + None, ), "titan" => { self.page.input.set_new_titan({ let client = self.client.clone(); let page = self.page.clone(); let redirects = self.redirects.clone(); - move |data, _label| { + move |data, on_failure| { handle( Request::Titan { uri: uri.clone(), @@ -93,27 +94,8 @@ impl Gemini { redirects.clone(), feature.clone(), cancellable.clone(), + Some(on_failure), ) - // init data to send - /* @TODO - use plurify::ns as plural; - - const CHUNK: usize = 0x400; - let bytes_sent = 0; - let bytes_total = data.len(); - - // send by chunks for large content size - if bytes_total > CHUNK { - label.set_label(&format!( - "sent {}/{} {}", - format_bytes(bytes_sent), - format_bytes(bytes_total), - plural(bytes_sent, &["byte", "bytes", "bytes"]) - )); - } else { - label.set_visible(false); - } - todo!()*/ } }); self.page.set_title("Titan input"); @@ -131,6 +113,7 @@ fn handle( redirects: Rc>, feature: Rc, cancellable: Cancellable, + on_failure: Option>, ) { let uri = request.uri().clone(); client.request_async( @@ -153,322 +136,320 @@ fn handle( { let page = page.clone(); let redirects = redirects.clone(); - move |result| { - // Remove input forms when redirection expected has not been applied (e.g. failure status) - // @TODO implement input data recovery on error (it's also available before unset, but reference lost at this point) - page.input.unset(); - - // Begin result handle - match result { - Ok(response) => { - match response.meta.status { - // https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected - Status::Input | Status::SensitiveInput => { - let title = match response.meta.data { - Some(data) => data.to_string(), - None => Status::Input.to_string(), - }; - if matches!(response.meta.status, Status::SensitiveInput) { - page.input.set_new_sensitive( - page.item_action.clone(), - uri, - Some(&title), - Some(1024), - ); - } else { - page.input.set_new_response( - page.item_action.clone(), - uri, - Some(&title), - Some(1024), - ); - } - page.set_progress(0.0); - page.set_title(&title); - redirects.replace(0); // reset + move |result| match result { + Ok(response) => { + match response.meta.status { + // https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected + Status::Input | Status::SensitiveInput => { + let title = match response.meta.data { + Some(data) => data.to_string(), + None => Status::Input.to_string(), + }; + if matches!(response.meta.status, Status::SensitiveInput) { + page.input.set_new_sensitive( + page.item_action.clone(), + uri, + Some(&title), + Some(1024), + ); + } else { + page.input.set_new_response( + page.item_action.clone(), + uri, + Some(&title), + Some(1024), + ); } - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-20 - Status::Success => match *feature { - Feature::Download => { - // Init download widget - let status = page.content.to_status_download( - uri_to_title(&uri).trim_matches(MAIN_SEPARATOR), // grab default filename from base URI, - // format FS entities - &cancellable, - { - let cancellable = cancellable.clone(); - let stream = response.connection.stream(); - move |file, action| { - match file.replace( - None, - false, - gtk::gio::FileCreateFlags::NONE, - Some(&cancellable), - ) { - Ok(file_output_stream) => { - use crate::tool::Format; - // Asynchronously read [IOStream](https://docs.gtk.org/gio/class.IOStream.html) - // to local [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) - // show bytes count in loading widget, validate max size for incoming data - // * no dependency of Gemini library here, feel free to use any other `IOStream` processor - ggemini::gio::file_output_stream::move_all_from_stream_async( - stream.clone(), - file_output_stream, - cancellable.clone(), - Priority::DEFAULT, - ( - 0x100000, // 1M bytes per chunk - None, // unlimited - 0, // initial totals - ), - ( - // on chunk - { - let action = action.clone(); - move |_, total| action.update.activate(&format!( - "Received {}...", - total.bytes() - )) - }, - // on complete - { - let action = action.clone(); - move |result| match result { - Ok((_, total)) => { - action.complete.activate(&format!( - "Saved to {} ({} total)", - file.parse_name(), - total.bytes() - )) - } - Err(e) => action.cancel.activate(&e.to_string()) + page.set_progress(0.0); + page.set_title(&title); + redirects.replace(0); // reset + } + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-20 + Status::Success => match *feature { + Feature::Download => { + // Init download widget + let status = page.content.to_status_download( + uri_to_title(&uri).trim_matches(MAIN_SEPARATOR), // grab default filename from base URI, + // format FS entities + &cancellable, + { + let cancellable = cancellable.clone(); + let stream = response.connection.stream(); + move |file, action| { + match file.replace( + None, + false, + gtk::gio::FileCreateFlags::NONE, + Some(&cancellable), + ) { + Ok(file_output_stream) => { + use crate::tool::Format; + // Asynchronously read [IOStream](https://docs.gtk.org/gio/class.IOStream.html) + // to local [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) + // show bytes count in loading widget, validate max size for incoming data + // * no dependency of Gemini library here, feel free to use any other `IOStream` processor + ggemini::gio::file_output_stream::move_all_from_stream_async( + stream.clone(), + file_output_stream, + cancellable.clone(), + Priority::DEFAULT, + ( + 0x100000, // 1M bytes per chunk + None, // unlimited + 0, // initial totals + ), + ( + // on chunk + { + let action = action.clone(); + move |_, total| action.update.activate(&format!( + "Received {}...", + total.bytes() + )) + }, + // on complete + { + let action = action.clone(); + move |result| match result { + Ok((_, total)) => { + action.complete.activate(&format!( + "Saved to {} ({} total)", + file.parse_name(), + total.bytes() + )) } - }, - ), - ) - } - Err(e) => action.cancel.activate(&e.to_string()), + Err(e) => action.cancel.activate(&e.to_string()) + } + }, + ), + ) } - } - }, - ); - page.set_progress(0.0); - page.set_title(&status.title()); - redirects.replace(0); // reset - }, - _ => match response.meta.mime { - Some(mime) => match mime.as_str() { - "text/gemini" => Text::from_stream_async( - response.connection.stream(), - Priority::DEFAULT, - cancellable.clone(), - move |result| match result { - Ok(text) => { - let widget = if matches!(*feature, Feature::Source) { - page.content.to_text_source(&text.to_string()) - } else { - page.content.to_text_gemini(&uri, &text.to_string()) - }; - page.search.set(Some(widget.text_view)); - page.set_title(&match widget.meta.title { - Some(title) => title.into(), // @TODO - None => uri_to_title(&uri), - }); - page.set_progress(0.0); - page.window_action - .find - .simple_action - .set_enabled(true); - redirects.replace(0); // reset - } - Err(e) => { - let status = page.content.to_status_failure(); - status.set_description(Some(&e.to_string())); - page.set_progress(0.0); - page.set_title(&status.title()); - redirects.replace(0); // reset - }, - }, - ), - "image/png" | "image/gif" | "image/jpeg" | "image/webp" => { - // Final image size unknown, show loading widget - let status = page.content.to_status_loading( - Some(Duration::from_secs(1)), // show if download time > 1 second - ); - - // Asynchronously read [IOStream](https://docs.gtk.org/gio/class.IOStream.html) - // to local [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) - // show bytes count in loading widget, validate max size for incoming data - // * no dependency of Gemini library here, feel free to use any other `IOStream` processor - ggemini::gio::memory_input_stream::from_stream_async( - response.connection.stream(), - cancellable.clone(), - Priority::DEFAULT, - 0x400, // 1024 bytes per chunk, optional step for images download tracking - 0xA00000, // 10M bytes max to prevent memory overflow if server play with promises - move |_, total| - status.set_description(Some(&format!("Download: {total} bytes"))), - { - let page = page.clone(); - move |result| match result { - Ok((memory_input_stream, _)) => { - Pixbuf::from_stream_async( - &memory_input_stream, - Some(&cancellable), - move |result| { - match result { - Ok(buffer) => { - page.set_title(&uri_to_title(&uri)); - page.content.to_image(&Texture::for_pixbuf(&buffer)); - } - Err(e) => { - let status = page.content.to_status_failure(); - status.set_description(Some(e.message())); - page.set_title(&status.title()); - } - } - page.set_progress(0.0); - redirects.replace(0); // reset - }, - ) - } - Err(e) => { - let status = page.content.to_status_failure(); - status.set_description(Some(&e.to_string())); - page.set_progress(0.0); - page.set_title(&status.title()); - redirects.replace(0); // reset - } - } - }, - ) - } - mime => { - let status = page - .content - .to_status_mime(mime, Some((&page.item_action, &uri))); - status.set_description(Some(&format!("Content type `{mime}` yet not supported"))); - page.set_progress(0.0); - page.set_title(&status.title()); - redirects.replace(0); // reset - }, - }, - None => { - let status = page.content.to_status_failure(); - status.set_description(Some("MIME type not found")); - page.set_progress(0.0); - page.set_title(&status.title()); - redirects.replace(0); // reset - }, - } - }, - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection - Status::PermanentRedirect | Status::Redirect => { - // Expected target URL in response meta - match response.meta.data { - Some(data) => match uri.parse_relative(data.as_str(), UriFlags::NONE) { - Ok(absolute) => { - // Base donor scheme could be `titan`, rewrite new relative links resolved with `gemini` - // otherwise, keep original scheme to handle external redirect rules properly - // * in this case, `titan` scheme redirects unexpected - let scheme = absolute.scheme(); - let target = Uri::build( - UriFlags::NONE, - &if "titan" == scheme { - scheme.replace("titan", "gemini") - } else { - scheme.to_string() - }, - absolute.userinfo().as_deref(), - absolute.host().as_deref(), - absolute.port(), - absolute.path().as_str(), - absolute.query().as_deref(), - absolute.fragment().as_deref(), - ); - // Increase client redirection counter - let total = redirects.take() + 1; - // Validate total redirects by protocol specification - if total > 5 { - let status = page.content.to_status_failure(); - status.set_description(Some("Redirection limit reached")); - page.set_progress(0.0); - page.set_title(&status.title()); - redirects.replace(0); // reset - - // Disallow external redirection by protocol restrictions - } else if "gemini" != target.scheme() - || uri.port() != target.port() - || uri.host() != target.host() { - let status = page.content.to_status_failure(); - status.set_description(Some("External redirects not allowed by protocol specification")); - page.set_progress(0.0); - page.set_title(&status.title()); - redirects.replace(0); // reset - // Valid - } else { - if matches!(response.meta.status, Status::PermanentRedirect) { - page.navigation.set_request(&uri.to_string()); - } - redirects.replace(total); - page.item_action.load.activate(Some(&target.to_string()), false); + Err(e) => action.cancel.activate(&e.to_string()), } } - Err(e) => { - let status = page.content.to_status_failure(); - status.set_description(Some(&e.to_string())); - page.set_progress(0.0); - page.set_title(&status.title()); - redirects.replace(0); // reset - } - } - None => { - let status = page.content.to_status_failure(); - status.set_description(Some("Redirection target not found")); - page.set_progress(0.0); - page.set_title(&status.title()); - redirects.replace(0); // reset - } - } - }, - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 - Status::CertificateRequest | - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized - Status::CertificateUnauthorized | - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid - Status::CertificateInvalid => { - let status = page.content.to_status_identity(); - status.set_description(Some(&match response.meta.data { - Some(data) => data.to_string(), - None => response.meta.status.to_string(), - })); - - page.set_progress(0.0); - page.set_title(&status.title()); - redirects.replace(0); // reset - } - error => { - let status = page.content.to_status_failure(); - status.set_description( - Some(&match response.meta.data { - Some(message) => message.to_string(), - None => error.to_string() - }) + }, ); page.set_progress(0.0); page.set_title(&status.title()); redirects.replace(0); // reset }, + _ => match response.meta.mime { + Some(mime) => match mime.as_str() { + "text/gemini" => Text::from_stream_async( + response.connection.stream(), + Priority::DEFAULT, + cancellable.clone(), + move |result| match result { + Ok(text) => { + let widget = if matches!(*feature, Feature::Source) { + page.content.to_text_source(&text.to_string()) + } else { + page.content.to_text_gemini(&uri, &text.to_string()) + }; + page.search.set(Some(widget.text_view)); + page.set_title(&match widget.meta.title { + Some(title) => title.into(), // @TODO + None => uri_to_title(&uri), + }); + page.set_progress(0.0); + page.window_action + .find + .simple_action + .set_enabled(true); + redirects.replace(0); // reset + } + Err(e) => { + let status = page.content.to_status_failure(); + status.set_description(Some(&e.to_string())); + page.set_progress(0.0); + page.set_title(&status.title()); + redirects.replace(0); // reset + }, + }, + ), + "image/png" | "image/gif" | "image/jpeg" | "image/webp" => { + // Final image size unknown, show loading widget + let status = page.content.to_status_loading( + Some(Duration::from_secs(1)), // show if download time > 1 second + ); + + // Asynchronously read [IOStream](https://docs.gtk.org/gio/class.IOStream.html) + // to local [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) + // show bytes count in loading widget, validate max size for incoming data + // * no dependency of Gemini library here, feel free to use any other `IOStream` processor + ggemini::gio::memory_input_stream::from_stream_async( + response.connection.stream(), + cancellable.clone(), + Priority::DEFAULT, + 0x400, // 1024 bytes per chunk, optional step for images download tracking + 0xA00000, // 10M bytes max to prevent memory overflow if server play with promises + move |_, total| + status.set_description(Some(&format!("Download: {total} bytes"))), + { + let page = page.clone(); + move |result| match result { + Ok((memory_input_stream, _)) => { + Pixbuf::from_stream_async( + &memory_input_stream, + Some(&cancellable), + move |result| { + match result { + Ok(buffer) => { + page.set_title(&uri_to_title(&uri)); + page.content.to_image(&Texture::for_pixbuf(&buffer)); + } + Err(e) => { + let status = page.content.to_status_failure(); + status.set_description(Some(e.message())); + page.set_title(&status.title()); + } + } + page.set_progress(0.0); + redirects.replace(0); // reset + }, + ) + } + Err(e) => { + let status = page.content.to_status_failure(); + status.set_description(Some(&e.to_string())); + page.set_progress(0.0); + page.set_title(&status.title()); + redirects.replace(0); // reset + } + } + }, + ) + } + mime => { + let status = page + .content + .to_status_mime(mime, Some((&page.item_action, &uri))); + status.set_description(Some(&format!("Content type `{mime}` yet not supported"))); + page.set_progress(0.0); + page.set_title(&status.title()); + redirects.replace(0); // reset + }, + }, + None => { + let status = page.content.to_status_failure(); + status.set_description(Some("MIME type not found")); + page.set_progress(0.0); + page.set_title(&status.title()); + redirects.replace(0); // reset + }, + } + }, + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection + Status::PermanentRedirect | Status::Redirect => { + // Expected target URL in response meta + match response.meta.data { + Some(data) => match uri.parse_relative(data.as_str(), UriFlags::NONE) { + Ok(absolute) => { + // Base donor scheme could be `titan`, rewrite new relative links resolved with `gemini` + // otherwise, keep original scheme to handle external redirect rules properly + // * in this case, `titan` scheme redirects unexpected + let scheme = absolute.scheme(); + let target = Uri::build( + UriFlags::NONE, + &if "titan" == scheme { + scheme.replace("titan", "gemini") + } else { + scheme.to_string() + }, + absolute.userinfo().as_deref(), + absolute.host().as_deref(), + absolute.port(), + absolute.path().as_str(), + absolute.query().as_deref(), + absolute.fragment().as_deref(), + ); + // Increase client redirection counter + let total = redirects.take() + 1; + // Validate total redirects by protocol specification + if total > 5 { + let status = page.content.to_status_failure(); + status.set_description(Some("Redirection limit reached")); + page.set_progress(0.0); + page.set_title(&status.title()); + redirects.replace(0); // reset + + // Disallow external redirection by protocol restrictions + } else if "gemini" != target.scheme() + || uri.port() != target.port() + || uri.host() != target.host() { + let status = page.content.to_status_failure(); + status.set_description(Some("External redirects not allowed by protocol specification")); + page.set_progress(0.0); + page.set_title(&status.title()); + redirects.replace(0); // reset + // Valid + } else { + if matches!(response.meta.status, Status::PermanentRedirect) { + page.navigation.set_request(&uri.to_string()); + } + redirects.replace(total); + page.item_action.load.activate(Some(&target.to_string()), false); + } + } + Err(e) => { + let status = page.content.to_status_failure(); + status.set_description(Some(&e.to_string())); + page.set_progress(0.0); + page.set_title(&status.title()); + redirects.replace(0); // reset + } + } + None => { + let status = page.content.to_status_failure(); + status.set_description(Some("Redirection target not found")); + page.set_progress(0.0); + page.set_title(&status.title()); + redirects.replace(0); // reset + } + } + }, + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 + Status::CertificateRequest | + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized + Status::CertificateUnauthorized | + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid + Status::CertificateInvalid => { + let status = page.content.to_status_identity(); + status.set_description(Some(&match response.meta.data { + Some(data) => data.to_string(), + None => response.meta.status.to_string(), + })); + + page.set_progress(0.0); + page.set_title(&status.title()); + redirects.replace(0); // reset } + error => { + let status = page.content.to_status_failure(); + + status.set_description( + Some(&match response.meta.data { + Some(message) => message.to_string(), + None => error.to_string() + }) + ); + page.set_progress(0.0); + page.set_title(&status.title()); + redirects.replace(0); // reset + + if let Some(callback) = on_failure { + callback() + } + }, } - Err(e) => { - let status = page.content.to_status_failure(); - status.set_description(Some(&e.to_string())); - page.set_progress(0.0); - page.set_title(&status.title()); - redirects.replace(0); // reset - } + } + Err(e) => { + let status = page.content.to_status_failure(); + status.set_description(Some(&e.to_string())); + page.set_progress(0.0); + page.set_title(&status.title()); + redirects.replace(0); // reset } } }, diff --git a/src/app/browser/window/tab/item/page/input.rs b/src/app/browser/window/tab/item/page/input.rs index 5e7f6e51..c30af99e 100644 --- a/src/app/browser/window/tab/item/page/input.rs +++ b/src/app/browser/window/tab/item/page/input.rs @@ -4,7 +4,7 @@ mod titan; use super::ItemAction; use adw::Clamp; -use gtk::{glib::Uri, prelude::WidgetExt, Box, Label}; +use gtk::{glib::Uri, prelude::WidgetExt}; use response::Response; use sensitive::Sensitive; use std::rc::Rc; @@ -40,7 +40,7 @@ impl Input { self.update(None); } - pub fn update(&self, child: Option<&Box>) { + pub fn update(&self, child: Option<>k::Box>) { if child.is_some() { self.clamp.set_visible(true); // widget may be hidden, make it visible to child redraw self.clamp.set_child(child); @@ -74,7 +74,7 @@ impl Input { )); } - pub fn set_new_titan(&self, on_send: impl Fn(&[u8], &Label) + 'static) { - self.update(Some(&Titan::build(on_send).g_box)); + pub fn set_new_titan(&self, on_send: impl Fn(&[u8], Box) + 'static) { + self.update(Some(>k::Box::titan(on_send))); } } diff --git a/src/app/browser/window/tab/item/page/input/titan.rs b/src/app/browser/window/tab/item/page/input/titan.rs index 102b3034..d540c1e9 100644 --- a/src/app/browser/window/tab/item/page/input/titan.rs +++ b/src/app/browser/window/tab/item/page/input/titan.rs @@ -3,36 +3,31 @@ mod form; mod title; use control::Control; +use control::Send; use form::Form; -use title::Title; - -use gtk::{gio::SimpleAction, glib::uuid_string_random, prelude::BoxExt, Box, Label, Orientation}; +use gtk::{ + prelude::{BoxExt, ButtonExt, TextBufferExt, TextViewExt}, + Label, Orientation, TextView, +}; use std::rc::Rc; +use title::Title; const MARGIN: i32 = 6; const SPACING: i32 = 8; -pub struct Titan { - // Components - pub g_box: Box, +pub trait Titan { + fn titan(callback: impl Fn(&[u8], Box) + 'static) -> Self; } -impl Titan { - // Constructors - - /// Build new `Self` - pub fn build(on_send: impl Fn(&[u8], &Label) + 'static) -> Self { - // Init local actions - let action_update = SimpleAction::new(&uuid_string_random(), None); - let action_send = SimpleAction::new(&uuid_string_random(), None); - +impl Titan for gtk::Box { + fn titan(callback: impl Fn(&[u8], Box) + 'static) -> Self { // Init components - let control = Rc::new(Control::build(action_send.clone())); - let form = Rc::new(Form::build(action_update.clone())); - let title = Title::build(None); + let control = Rc::new(Control::build()); + let form = TextView::form(); + let title = Label::title(None); // Init widget - let g_box = Box::builder() + let g_box = gtk::Box::builder() .margin_bottom(MARGIN) .margin_end(MARGIN) .margin_start(MARGIN) @@ -41,21 +36,31 @@ impl Titan { .orientation(Orientation::Vertical) .build(); - g_box.append(&title.label); - g_box.append(&form.text_view); + g_box.append(&title); + g_box.append(&form); g_box.append(&control.g_box); - // Init events - action_update.connect_activate({ - let control = control.clone(); + // Connect events + control.send.connect_clicked({ let form = form.clone(); - move |_, _| control.update(Some(form.text().len())) + move |this| { + this.set_sending(); + callback( + form.text().as_bytes(), + Box::new({ + let this = this.clone(); + move || this.set_resend() // on failure + }), + ) + } }); - action_send - .connect_activate(move |_, _| on_send(form.text().as_bytes(), &control.counter.label)); + form.buffer().connect_changed({ + let control = control.clone(); + move |this| control.update(Some(this.char_count())) + }); - // Return activated struct - Self { g_box } + // Return activated `Self` + g_box } } 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 index 6c61b6d1..12597e56 100644 --- a/src/app/browser/window/tab/item/page/input/titan/control.rs +++ b/src/app/browser/window/tab/item/page/input/titan/control.rs @@ -2,16 +2,17 @@ mod counter; mod send; use counter::Counter; -use gtk::gio::SimpleAction; -use gtk::{prelude::BoxExt, Align, Box, Orientation}; -use send::Send; -use std::rc::Rc; +use gtk::{ + prelude::{BoxExt, WidgetExt}, + Align, Box, Button, Label, Orientation, +}; +pub use send::Send; const SPACING: i32 = 8; pub struct Control { - pub counter: Rc, - pub send: Rc, + pub counter: Label, + pub send: Button, pub g_box: Box, } @@ -19,10 +20,10 @@ impl Control { // Constructors /// Build new `Self` - pub fn build(action_send: SimpleAction) -> Self { + pub fn build() -> Self { // Init components - let counter = Rc::new(Counter::new()); - let send = Rc::new(Send::build(action_send)); + let counter = Label::counter(); + let send = Button::send(); // Init main widget let g_box = Box::builder() @@ -31,8 +32,8 @@ impl Control { .spacing(SPACING) .build(); - g_box.append(&counter.label); - g_box.append(&send.button); + g_box.append(&counter); + g_box.append(&send); // Return activated struct Self { @@ -43,10 +44,10 @@ impl Control { } // Actions - pub fn update(&self, bytes_total: Option) { + pub fn update(&self, char_count: Option) { // Update children components - self.counter.update(bytes_total); - self.send.update(match bytes_total { + self.counter.update(char_count); + self.send.set_sensitive(match char_count { Some(total) => total > 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 index ae61f391..f2ff2898 100644 --- 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 @@ -1,31 +1,26 @@ use gtk::{prelude::WidgetExt, Label}; -pub struct Counter { - pub label: Label, +pub trait Counter { + fn counter() -> Self; + fn update(&self, char_count: Option); } -impl Default for Counter { - fn default() -> Self { - Self::new() - } -} +impl Counter for Label { + // Constructors -impl Counter { - // Construct - pub fn new() -> Self { - Self { - label: Label::builder().css_classes(["dim-label"]).build(), // @TODO use `dimmed` in Adw 1.6, - } + fn counter() -> Self { + Label::builder().css_classes(["dim-label"]).build() // @TODO use `dimmed` in Adw 1.6, } // Actions - pub fn update(&self, bytes_total: Option) { - match bytes_total { + + fn update(&self, char_count: Option) { + match char_count { Some(value) => { - self.label.set_label(&value.to_string()); - self.label.set_visible(value > 0); + self.set_label(&value.to_string()); + self.set_visible(value > 0); } - None => self.label.set_visible(false), + None => self.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 index 6eefdaa3..e465330d 100644 --- 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 @@ -1,40 +1,28 @@ use gtk::{ - gio::SimpleAction, - prelude::{ActionExt, ButtonExt, WidgetExt}, + prelude::{ButtonExt, WidgetExt}, Button, }; -pub struct Send { - pub button: Button, +pub trait Send { + fn send() -> Self; + fn set_sending(&self); + fn set_resend(&self); } -impl Send { - // Constructors - - /// Build new `Self` - pub fn build(action_send: SimpleAction) -> Self { - // Init main widget - let button = Button::builder() +impl Send for Button { + fn send() -> Self { + Button::builder() .css_classes(["accent"]) // | `suggested-action` .label("Send") .sensitive(false) - .build(); - - // Init events - button.connect_clicked({ - move |this| { - this.set_sensitive(false); - this.set_label("sending.."); - action_send.activate(None); - } - }); - - // Return activated `Self` - Self { button } + .build() } - - // Actions - pub fn update(&self, is_sensitive: bool) { - self.button.set_sensitive(is_sensitive); + fn set_sending(&self) { + self.set_sensitive(false); + self.set_label("sending.."); + } + fn set_resend(&self) { + self.set_sensitive(true); + self.set_label("Resend"); } } 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 index b5ed8d73..0ab7cd9d 100644 --- a/src/app/browser/window/tab/item/page/input/titan/form.rs +++ b/src/app/browser/window/tab/item/page/input/titan/form.rs @@ -1,7 +1,6 @@ use gtk::{ - gio::SimpleAction, glib::GString, - prelude::{ActionExt, TextBufferExt, TextViewExt, WidgetExt}, + prelude::{TextBufferExt, TextViewExt, WidgetExt}, TextView, WrapMode, }; use libspelling::{Checker, TextBufferAdapter}; @@ -9,15 +8,16 @@ use sourceview::Buffer; const MARGIN: i32 = 8; -pub struct Form { - pub text_view: TextView, +pub trait Form { + fn form() -> Self; + fn text(&self) -> GString; } -impl Form { +impl Form for TextView { // Constructors /// Build new `Self` - pub fn build(action_update: SimpleAction) -> Self { + fn form() -> Self { // Init [SourceView](https://gitlab.gnome.org/GNOME/gtksourceview) type buffer let buffer = Buffer::builder().build(); @@ -43,22 +43,18 @@ impl Form { text_view.set_size_request(-1, 38); // @TODO [#635](https://gitlab.gnome.org/GNOME/pygobject/-/issues/635) // Init events - text_view.buffer().connect_changed(move |_| { - action_update.activate(None); - }); - text_view.connect_realize(|this| { this.grab_focus(); }); // Return activated `Self` - Self { text_view } + text_view } // Getters - pub fn text(&self) -> GString { - let buffer = self.text_view.buffer(); + fn text(&self) -> GString { + let buffer = self.buffer(); buffer.text(&buffer.start_iter(), &buffer.end_iter(), true) } } 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 index c912d00b..535c73c6 100644 --- a/src/app/browser/window/tab/item/page/input/titan/title.rs +++ b/src/app/browser/window/tab/item/page/input/titan/title.rs @@ -1,20 +1,15 @@ use gtk::{Align, Label}; -pub struct Title { - pub label: Label, +pub trait Title { + fn title(title: Option<&str>) -> Self; } -impl Title { - // Constructors - - /// Build new `Self` - pub fn build(title: Option<&str>) -> Self { - Self { - label: Label::builder() - .css_classes(["heading"]) - .halign(Align::Start) - .label(title.unwrap_or("Titan input")) - .build(), - } +impl Title for Label { + fn title(title: Option<&str>) -> Self { + Label::builder() + .css_classes(["heading"]) + .halign(Align::Start) + .label(title.unwrap_or("Titan input")) + .build() } }