feat: let composes carry custom frontmatter headers
This commit is contained in:
parent
74ec9a2492
commit
68f5620d35
5 changed files with 401 additions and 10 deletions
98
src/view.rs
98
src/view.rs
|
|
@ -202,6 +202,7 @@ pub struct CarrierWidgets {
|
|||
|
||||
// compose page
|
||||
compose_to: adw::EntryRow,
|
||||
compose_headers: gtk::ListBox,
|
||||
compose_subject: adw::EntryRow,
|
||||
compose_body: gtk::TextBuffer,
|
||||
compose_send: adw::ButtonContent,
|
||||
|
|
@ -531,6 +532,14 @@ impl CarrierWidgets {
|
|||
compose_fields.append(&compose_to);
|
||||
compose_fields.append(&compose_subject);
|
||||
compose.append(&compose_fields);
|
||||
// Frontmatter rows: one `Key: value` entry each, plus a flat
|
||||
// add button below. Rows are rebuilt in render on structural
|
||||
// changes only, so the entries never lose focus while typing.
|
||||
let compose_headers = boxed_list();
|
||||
compose.append(&compose_headers);
|
||||
let add_header = button("+ header", "quiet");
|
||||
add_header.set_halign(Align::Start);
|
||||
compose.append(&add_header);
|
||||
let compose_view = gtk::TextView::new();
|
||||
compose_view.set_wrap_mode(WrapMode::WordChar);
|
||||
compose_view.set_top_margin(8);
|
||||
|
|
@ -890,6 +899,8 @@ impl CarrierWidgets {
|
|||
gtk::glib::Propagation::Stop
|
||||
});
|
||||
|
||||
add_header.connect_clicked(input(Message::ComposeHeaderAdded));
|
||||
|
||||
compose_send_button.connect_clicked(input(Message::SendPressed));
|
||||
compose_cancel.connect_clicked(input(Message::ComposeClosed));
|
||||
let to_sender = sender.clone();
|
||||
|
|
@ -991,6 +1002,7 @@ impl CarrierWidgets {
|
|||
article_body,
|
||||
actions,
|
||||
compose_to,
|
||||
compose_headers,
|
||||
compose_subject,
|
||||
compose_body,
|
||||
compose_send,
|
||||
|
|
@ -1128,6 +1140,7 @@ fn spawn_op(model: &Carrier, op: PendingOp, sender: &ComponentSender<Carrier>) {
|
|||
subject,
|
||||
body,
|
||||
reply_to,
|
||||
headers,
|
||||
} => sender.spawn_command(move |out| {
|
||||
let res = ops::send(
|
||||
&store,
|
||||
|
|
@ -1140,6 +1153,7 @@ fn spawn_op(model: &Carrier, op: PendingOp, sender: &ComponentSender<Carrier>) {
|
|||
},
|
||||
&body,
|
||||
reply_to.as_deref(),
|
||||
&headers,
|
||||
timeout,
|
||||
);
|
||||
let _ = out.send(OpResult::Generic(res));
|
||||
|
|
@ -1203,15 +1217,44 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender<Carr
|
|||
move |_: >k::Button| sender.input(msg.clone())
|
||||
};
|
||||
|
||||
// Prefill entries once after a reply or write-to-contact; afterwards
|
||||
// typing flows entry -> model only, so the view never fights the user.
|
||||
// Prefill entries once after a reply, write or draft reopen;
|
||||
// afterwards typing flows entry -> model only, so the view never
|
||||
// fights the user. The fresh flag also rebuilds the header rows,
|
||||
// which otherwise sync on count changes alone.
|
||||
let mut compose_fresh = false;
|
||||
if model.compose_fresh.take() {
|
||||
compose_fresh = true;
|
||||
if let Some(compose) = &model.compose {
|
||||
w.compose_to.set_text(&compose.to);
|
||||
w.compose_subject.set_text(&compose.subject);
|
||||
w.compose_body.set_text(&compose.body);
|
||||
}
|
||||
}
|
||||
let rows = model.compose.as_ref().map_or(0, |c| c.headers.len());
|
||||
let mut current = 0;
|
||||
let mut child = w.compose_headers.first_child();
|
||||
while let Some(item) = child {
|
||||
current += 1;
|
||||
child = item.next_sibling();
|
||||
}
|
||||
if compose_fresh || current != rows {
|
||||
let added = rows > current;
|
||||
clear_list(&w.compose_headers);
|
||||
if let Some(compose) = &model.compose {
|
||||
let mut last_entry = None;
|
||||
for (index, text) in compose.headers.iter().enumerate() {
|
||||
let (row, entry) = compose_header_row(index, text, sender);
|
||||
last_entry = Some(entry);
|
||||
w.compose_headers.append(&row);
|
||||
}
|
||||
// A freshly appended row is the one being typed into.
|
||||
if added {
|
||||
if let Some(entry) = last_entry {
|
||||
entry.grab_focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Header and sidebar identity.
|
||||
let address = model
|
||||
|
|
@ -1711,6 +1754,57 @@ fn mail_row(folder: Folder, row: &Row) -> adw::ActionRow {
|
|||
item
|
||||
}
|
||||
|
||||
/// One compose header row: a `Key: value` entry plus a remove button.
|
||||
/// The row carries its own validation state — anything the send would
|
||||
/// reject is tinted and explained right where it is typed, and the
|
||||
/// empty rows the send skips are left quiet.
|
||||
fn compose_header_row<'a>(
|
||||
index: usize,
|
||||
text: &str,
|
||||
sender: &ComponentSender<Carrier>,
|
||||
) -> (gtk::ListBoxRow, gtk::Entry) {
|
||||
let entry = gtk::Entry::new();
|
||||
entry.set_hexpand(true);
|
||||
entry.set_placeholder_text(Some("Key: value"));
|
||||
entry.set_text(text);
|
||||
let style = |entry: >k::Entry, text: &str| match crate::ops::header_row(text) {
|
||||
Err(reason) => {
|
||||
entry.add_css_class("error");
|
||||
entry.set_tooltip_text(Some(&reason));
|
||||
}
|
||||
Ok(_) => {
|
||||
entry.remove_css_class("error");
|
||||
entry.set_tooltip_text(None);
|
||||
}
|
||||
};
|
||||
style(&entry, text);
|
||||
let change_sender = sender.clone();
|
||||
let style_entry = entry.clone();
|
||||
entry.connect_changed(move |changed| {
|
||||
let text = changed.text().to_string();
|
||||
style(&style_entry, &text);
|
||||
change_sender.input(Message::ComposeHeaderChanged(index, text));
|
||||
});
|
||||
|
||||
let remove = icon_button("user-trash-symbolic", "Remove this header");
|
||||
let remove_sender = sender.clone();
|
||||
remove.connect_clicked(move |_| {
|
||||
remove_sender.input(Message::ComposeHeaderRemoved(index));
|
||||
});
|
||||
|
||||
let line = gtk::Box::new(Orientation::Horizontal, 6);
|
||||
line.set_margin_top(6);
|
||||
line.set_margin_bottom(6);
|
||||
line.set_margin_start(12);
|
||||
line.set_margin_end(6);
|
||||
line.append(&entry);
|
||||
line.append(&remove);
|
||||
|
||||
let row = gtk::ListBoxRow::new();
|
||||
row.set_child(Some(&line));
|
||||
(row, entry)
|
||||
}
|
||||
|
||||
/// One contact in the sidebar's bottom panel: the address over its trust
|
||||
/// state; activating it opens the detail pane.
|
||||
fn sidebar_contact_row(contact: &Contact) -> adw::ActionRow {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue