reorganize shared input components

This commit is contained in:
yggverse 2025-02-07 19:32:49 +02:00
parent 266d29b336
commit 41d30f82fd
16 changed files with 238 additions and 380 deletions

View File

@ -75,6 +75,6 @@ impl Input {
}
pub fn set_new_titan(&self, on_send: impl Fn(titan::Header, Bytes, Box<dyn Fn()>) + 'static) {
self.update(Some(&gtk::Notebook::titan(on_send)));
self.update(Some(&gtk::Box::titan(on_send)));
}
}

View File

@ -1,35 +1,93 @@
mod control;
mod file;
mod header;
mod tab;
mod text;
mod title;
use control::Control;
use file::File;
use gtk::{
glib::{uuid_string_random, Bytes},
Notebook,
};
use gtk::{glib::Bytes, Notebook};
pub use header::Header;
use tab::Tab;
use text::Text;
use title::Title;
pub trait Titan {
fn titan(callback: impl Fn(Header, Bytes, Box<dyn Fn()>) + 'static) -> Self;
}
impl Titan for Notebook {
impl Titan for gtk::Box {
fn titan(callback: impl Fn(Header, Bytes, Box<dyn Fn()>) + 'static) -> Self {
use gtk::{Box, Label};
use gtk::{glib::uuid_string_random, Box, Label, TextView};
use std::{cell::Cell, rc::Rc};
let notebook = Notebook::builder()
.name(format!("s{}", uuid_string_random()))
.show_border(false)
.build();
// Init components
let header = Rc::new(Cell::new(Header {
mime: None,
token: None,
}));
let control = Rc::new(Control::build(&header));
notebook.append_page(&Box::text(callback), Some(&Label::title("Text")));
notebook.append_page(&Box::file(), Some(&Label::title("File")));
let text = TextView::text(&control);
let file = File::build(&control);
notebook_css_patch(&notebook);
notebook
let notebook = {
let notebook = Notebook::builder()
.name(format!("s{}", uuid_string_random()))
.show_border(false)
.build();
notebook.append_page(&text, Some(&Label::tab("Text")));
notebook.append_page(&file.button, Some(&Label::tab("File")));
notebook.connect_switch_page({
let control = control.clone();
let text = text.clone();
move |_, _, i| {
if i == 0 {
control.update(Some(text.len()), Some(text.count()))
} else {
control.update(file.size(), None)
}
}
});
notebook_css_patch(&notebook);
notebook
};
// Init main widget
let g_box = {
use gtk::{prelude::BoxExt, Orientation};
let g_box = {
const MARGIN: i32 = 8;
Box::builder()
.margin_end(MARGIN)
.margin_start(MARGIN)
.orientation(Orientation::Vertical)
.spacing(MARGIN)
.build()
};
g_box.append(&notebook);
g_box.append(&control.g_box);
g_box
};
// Init events
/*control.upload.connect_clicked(move |this| {
this.set_uploading();
callback(
header.take(),
Bytes::from(form.text().as_bytes()),
Box::new({
let this = this.clone();
move || this.set_resend() // on failure
}),
)
});*/
g_box
}
}

View File

@ -12,8 +12,6 @@ use options::Options;
use std::{cell::Cell, rc::Rc};
pub use upload::Upload;
const SPACING: i32 = 8;
pub struct Control {
pub counter: Label,
pub upload: Button,
@ -31,11 +29,15 @@ impl Control {
let upload = Button::upload();
// Init main widget
let g_box = Box::builder()
.halign(Align::End)
.orientation(Orientation::Horizontal)
.spacing(SPACING)
.build();
let g_box = {
const MARGIN: i32 = 8;
Box::builder()
.halign(Align::End)
.margin_bottom(MARGIN)
.orientation(Orientation::Horizontal)
.spacing(MARGIN)
.build()
};
g_box.append(&counter);
g_box.append(&options);
@ -50,9 +52,10 @@ impl Control {
}
// Actions
pub fn update(&self, chars_count: i32, bytes_total: usize) {
pub fn update(&self, bytes_total: Option<usize>, chars_count: Option<i32>) {
// Update children components
self.counter.update(chars_count, bytes_total);
self.upload.set_sensitive(bytes_total > 0);
self.counter.update(bytes_total, chars_count);
self.upload
.set_sensitive(bytes_total.is_some_and(|this| this > 0));
}
}

View File

@ -0,0 +1,54 @@
use gtk::Label;
pub trait Counter {
fn counter() -> Self;
fn update(&self, bytes_total: Option<usize>, chars_count: Option<i32>);
}
impl Counter for Label {
// Constructors
fn counter() -> Self {
Label::builder().css_classes(["dim-label"]).build() // @TODO use `dimmed` in Adw 1.6,
}
// Actions
fn update(&self, bytes_total: Option<usize>, chars_count: Option<i32>) {
use gtk::prelude::WidgetExt;
self.set_visible(if let Some(bytes_total) = bytes_total {
if let Some(chars_count) = chars_count {
if chars_count > 0 {
self.set_label(&bytes_total.to_string());
self.set_tooltip_markup(Some(&format_text_tooltip(bytes_total, chars_count)));
true
} else {
false
}
} else {
self.set_label(&format_file_tooltip(bytes_total));
self.set_tooltip_markup(None);
true
}
} else {
false
});
}
}
// Tools
fn format_file_tooltip(bytes_total: usize) -> String {
use crate::tool::Format;
bytes_total.bytes()
}
fn format_text_tooltip(bytes_total: usize, chars_count: i32) -> String {
use plurify::Plurify;
format!(
"{bytes_total} {} <sup>/ {chars_count} {}</sup>",
(bytes_total).plurify(&["byte", "bytes", "bytes"]),
(chars_count as usize).plurify(&["char", "chars", "chars"]),
)
}

View File

@ -1,97 +1,86 @@
mod control;
mod form;
use super::Control;
use gtk::{glib::Bytes, Button};
use std::{cell::RefCell, rc::Rc};
use super::Header;
use control::Control;
use gtk::Box;
pub trait File {
fn file() -> Self;
pub struct File {
buffer: Rc<RefCell<Option<Bytes>>>,
pub button: Button,
}
impl File for Box {
fn file() -> Self {
use form::Form;
impl File {
pub fn build(control: &Rc<Control>) -> Self {
use gtk::{
gio::Cancellable,
prelude::{ButtonExt, FileExt, WidgetExt},
Button, FileDialog, Window,
};
use std::{cell::Cell, rc::Rc};
// Init components
let header = Rc::new(Cell::new(Header {
mime: None,
token: None,
}));
let control = Rc::new(Control::build(&header));
let form = Button::form();
let buffer = Rc::new(RefCell::new(None));
// Init main widget
let g_box = {
use gtk::{prelude::BoxExt, Orientation};
const MARGIN: i32 = 8;
let g_box = Box::builder()
.margin_end(MARGIN)
.margin_start(MARGIN)
.orientation(Orientation::Vertical)
.spacing(MARGIN)
.build();
g_box.append(&form);
g_box.append(&control.g_box);
g_box
};
let button = Button::builder()
.label("Choose a file..")
.margin_top(4)
.build();
// Init events
form.connect_clicked(move |form| {
const CLASS: (&str, &str, &str) = ("error", "warning", "success");
button.connect_clicked({
let control = control.clone();
let buffer = buffer.clone();
move |this| {
const CLASS: (&str, &str, &str) = ("error", "warning", "success");
// reset
control.update(None);
form.set_sensitive(false);
form.remove_css_class(CLASS.0);
form.remove_css_class(CLASS.1);
form.remove_css_class(CLASS.2);
// reset
control.update(None, None);
this.set_sensitive(false);
this.remove_css_class(CLASS.0);
this.remove_css_class(CLASS.1);
this.remove_css_class(CLASS.2);
FileDialog::builder()
.build()
.open(Window::NONE, Cancellable::NONE, {
let control = control.clone();
let form = form.clone();
move |result| match result {
Ok(file) => match file.path() {
Some(path) => {
form.set_label("Buffering, please wait..");
file.load_bytes_async(Cancellable::NONE, move |result| match result
{
Ok((bytes, _)) => {
control.update(Some(bytes.len()));
FileDialog::builder()
.build()
.open(Window::NONE, Cancellable::NONE, {
let control = control.clone();
let buffer = buffer.clone();
let this = this.clone();
move |result| match result {
Ok(file) => match file.path() {
Some(path) => {
this.set_label("Buffering, please wait.."); // @TODO progress
file.load_bytes_async(Cancellable::NONE, move |result| {
match result {
Ok((bytes, _)) => {
control.update(Some(bytes.len()), None);
buffer.replace(Some(bytes));
form.set_label(path.to_str().unwrap());
form.set_css_classes(&[CLASS.2]);
form.set_sensitive(true);
}
Err(e) => {
form.set_css_classes(&[CLASS.0]);
form.set_label(e.message());
form.set_sensitive(true);
}
})
this.set_css_classes(&[CLASS.2]);
this.set_label(path.to_str().unwrap());
this.set_sensitive(true);
}
Err(e) => {
this.set_css_classes(&[CLASS.0]);
this.set_label(e.message());
this.set_sensitive(true);
}
}
})
}
None => todo!(),
},
Err(e) => {
this.set_css_classes(&[CLASS.1]);
this.set_label(e.message());
this.set_sensitive(true);
}
None => todo!(),
},
Err(e) => {
form.set_css_classes(&[CLASS.1]);
form.set_label(e.message());
form.set_sensitive(true);
}
}
});
});
}
});
g_box
Self { buffer, button }
}
pub fn size(&self) -> Option<usize> {
self.buffer.borrow().as_ref().map(|bytes| bytes.len())
}
}

View File

@ -1,58 +0,0 @@
mod counter;
mod options;
mod upload;
use super::Header;
use counter::Counter;
use gtk::{Box, Button, Label};
use options::Options;
use std::{cell::Cell, rc::Rc};
use upload::Upload;
pub struct Control {
counter: Label,
options: Button,
upload: Button,
pub g_box: Box,
}
impl Control {
pub fn build(header: &Rc<Cell<Header>>) -> Self {
// Init components
let counter = Label::counter();
let options = Button::options(header);
let upload = Button::upload();
// Init main widget
let g_box = {
use gtk::{prelude::BoxExt, Align, Orientation};
let g_box = Box::builder()
.halign(Align::End)
.orientation(Orientation::Horizontal)
.spacing(8)
.build();
g_box.append(&counter);
g_box.append(&options);
g_box.append(&upload);
g_box
};
Self {
counter,
options,
upload,
g_box,
}
}
pub fn update(&self, bytes_total: Option<usize>) {
use gtk::prelude::WidgetExt;
self.counter.update(bytes_total);
let is_some = bytes_total.is_some();
self.options.set_sensitive(is_some);
self.upload.set_sensitive(is_some);
}
}

View File

@ -1,26 +0,0 @@
use gtk::{prelude::WidgetExt, Label};
pub trait Counter {
fn counter() -> Self;
fn update(&self, bytes_total: Option<usize>);
}
impl Counter for Label {
// Constructors
fn counter() -> Self {
Label::builder().css_classes(["dim-label"]).build() // @TODO use `dimmed` in Adw 1.6,
}
// Actions
fn update(&self, bytes_total: Option<usize>) {
self.set_visible(if let Some(bytes_total) = bytes_total {
use crate::tool::Format;
self.set_text(&bytes_total.bytes());
true
} else {
false
})
}
}

View File

@ -1,38 +0,0 @@
use super::Header;
use gtk::{
prelude::{ButtonExt, WidgetExt},
Button,
};
use std::{cell::Cell, rc::Rc};
pub trait Options {
fn options(header: &Rc<Cell<Header>>) -> Self;
}
impl Options for Button {
fn options(header: &Rc<Cell<Header>>) -> Self {
let button = Button::builder()
.icon_name("emblem-system-symbolic")
// @TODO deactivate by default on dyn MIME type detection only
// .sensitive(false)
.tooltip_text("Options")
.build();
button.connect_clicked({
let header = header.clone();
move |this| {
this.set_sensitive(false); // lock
header.take().dialog(Some(this), {
let this = this.clone();
let header = header.clone();
move |options| {
header.replace(options);
this.set_sensitive(true); // unlock
}
})
}
});
button
}
}

View File

@ -1,22 +0,0 @@
use gtk::Button;
pub trait Form {
fn form() -> Self;
}
impl Form for Button {
fn form() -> Self {
use gtk::prelude::{ButtonExt, WidgetExt};
let button = Button::builder()
.label("Choose a file..")
.margin_top(4)
.build();
button.connect_clicked(|this| {
this.set_sensitive(false); // lock
});
button
}
}

View File

@ -1,11 +1,11 @@
use gtk::Label;
pub trait Title {
fn title(label: &str) -> Self;
pub trait Tab {
fn tab(label: &str) -> Self;
}
impl Title for Label {
fn title(label: &str) -> Self {
impl Tab for Label {
fn tab(label: &str) -> Self {
Label::builder()
.css_classes(["heading"])
.label(label)

View File

@ -1,71 +1,42 @@
mod control;
mod form;
use super::Header;
use gtk::glib::Bytes;
use super::Control;
use gtk::{
prelude::{TextBufferExt, TextViewExt},
TextView,
};
use std::rc::Rc;
pub trait Text {
fn text(callback: impl Fn(Header, Bytes, Box<dyn Fn()>) + 'static) -> Self;
fn text(control: &Rc<Control>) -> Self;
fn len(&self) -> usize;
fn count(&self) -> i32;
}
impl Text for gtk::Box {
fn text(callback: impl Fn(Header, Bytes, Box<dyn Fn()>) + 'static) -> Self {
use control::{Control, Upload};
impl Text for TextView {
fn text(control: &Rc<Control>) -> Self {
use form::Form;
use gtk::{
prelude::{BoxExt, ButtonExt, TextBufferExt, TextViewExt},
Orientation, TextView,
};
use std::{cell::Cell, rc::Rc};
// Init components
let header = Rc::new(Cell::new(Header {
mime: Some("text/plain".into()), // some servers require not empty content type
token: None,
}));
let control = Rc::new(Control::build(&header));
let form = TextView::form();
let text_view = TextView::form();
// Init widget
let g_box = {
const MARGIN: i32 = 8;
let g_box = gtk::Box::builder()
.margin_bottom(MARGIN / 2)
.margin_end(MARGIN)
.margin_start(MARGIN)
.orientation(Orientation::Vertical)
.spacing(MARGIN)
.build();
g_box.append(&form);
g_box.append(&control.g_box);
g_box
};
// Connect events
form.buffer().connect_changed({
text_view.buffer().connect_changed({
let control = control.clone();
move |this| {
control.update(
this.char_count(),
this.text(&this.start_iter(), &this.end_iter(), true).len(),
)
}
let text_view = text_view.clone();
move |text_buffer| control.update(Some(text_view.len()), Some(text_buffer.char_count()))
});
control.upload.connect_clicked(move |this| {
this.set_uploading();
callback(
header.take(),
Bytes::from(form.text().as_bytes()),
Box::new({
let this = this.clone();
move || this.set_resend() // on failure
}),
)
});
text_view
}
g_box
fn count(&self) -> i32 {
self.buffer().char_count()
}
fn len(&self) -> usize {
let buffer = self.buffer();
buffer
.text(&buffer.start_iter(), &buffer.end_iter(), true)
.len()
}
}

View File

@ -1,31 +0,0 @@
use gtk::{prelude::WidgetExt, Label};
use plurify::Plurify;
pub trait Counter {
fn counter() -> Self;
fn update(&self, char_count: i32, bytes_total: usize);
}
impl Counter for Label {
// Constructors
fn counter() -> Self {
Label::builder().css_classes(["dim-label"]).build() // @TODO use `dimmed` in Adw 1.6,
}
// Actions
fn update(&self, chars_count: i32, bytes_total: usize) {
self.set_visible(if bytes_total > 0 {
self.set_label(&bytes_total.to_string());
self.set_tooltip_markup(Some(&format!(
"{bytes_total} {} <sup>/ {chars_count} {}</sup>",
(bytes_total).plurify(&["byte", "bytes", "bytes"]),
(chars_count as usize).plurify(&["char", "chars", "chars"]),
)));
true
} else {
false
})
}
}

View File

@ -1,30 +0,0 @@
use gtk::{
prelude::{ButtonExt, WidgetExt},
Button,
};
pub trait Upload {
fn upload() -> Self;
fn set_uploading(&self);
fn set_resend(&self);
}
impl Upload for Button {
fn upload() -> Self {
Button::builder()
// @TODO this class not looks well with default GTK Notebook widget
// activate it after upgrade to `ToggleGroup` in Adw v1.7 / Ubuntu 26.04
// .css_classes(["accent"]) // | `suggested-action`
.label("Upload")
.sensitive(false)
.build()
}
fn set_uploading(&self) {
self.set_sensitive(false);
self.set_label("uploading..");
}
fn set_resend(&self) {
self.set_sensitive(true);
self.set_label("Resend");
}
}

View File

@ -1,14 +1,9 @@
use gtk::{
glib::GString,
prelude::{TextBufferExt, TextViewExt, WidgetExt},
TextView, WrapMode,
};
use gtk::{prelude::WidgetExt, TextView, WrapMode};
use libspelling::{Checker, TextBufferAdapter};
use sourceview::Buffer;
pub trait Form {
fn form() -> Self;
fn text(&self) -> GString;
}
impl Form for TextView {
@ -51,11 +46,4 @@ impl Form for TextView {
// Return activated `Self`
text_view
}
// Getters
fn text(&self) -> GString {
let buffer = self.buffer();
buffer.text(&buffer.start_iter(), &buffer.end_iter(), true)
}
}