feat: adopt a notational-velocity layout with larger text
This commit is contained in:
parent
7ed2889966
commit
517d938c46
1 changed files with 106 additions and 122 deletions
228
src/view.rs
228
src/view.rs
|
|
@ -14,13 +14,18 @@ use crate::app::{Carrier, Dialog, Message, OnboardMode, Page};
|
|||
use crate::ops::{self, Contact, Folder, Row};
|
||||
use crate::theme::{self, ACCENT, MUTED, TEXT};
|
||||
|
||||
/// Column widths of the list, in pixels; monospace keeps them aligned.
|
||||
const DATE_W: f32 = 104.0;
|
||||
const FLAG_W: f32 = 24.0;
|
||||
const FROM_W: f32 = 248.0;
|
||||
const ADDR_W: f32 = 260.0;
|
||||
const KEY_W: f32 = 248.0;
|
||||
const STATE_W: f32 = 88.0;
|
||||
/// Text sizes: rows and controls read at 14, the article body at 15, the
|
||||
/// selected subject as a 16 heading — the sizes of the notational-velocity
|
||||
/// family this layout borrows from.
|
||||
const S: f32 = 14.0;
|
||||
const BODY: f32 = 15.0;
|
||||
const BIG: f32 = 16.0;
|
||||
|
||||
/// Column widths of the contacts table, in pixels; monospace keeps them
|
||||
/// aligned.
|
||||
const ADDR_W: f32 = 300.0;
|
||||
const KEY_W: f32 = 300.0;
|
||||
const STATE_W: f32 = 110.0;
|
||||
|
||||
pub fn view(app: &Carrier) -> Element<'_, Message> {
|
||||
let content: Element<'_, Message> = if app.compose.is_some() {
|
||||
|
|
@ -47,21 +52,21 @@ pub fn view(app: &Carrier) -> Element<'_, Message> {
|
|||
fn mono<'a>(content: impl std::fmt::Display) -> iced::widget::Text<'a> {
|
||||
text(content.to_string())
|
||||
.font(Font::MONOSPACE)
|
||||
.size(12)
|
||||
.size(S)
|
||||
.color(TEXT)
|
||||
}
|
||||
|
||||
fn muted<'a>(content: impl std::fmt::Display) -> iced::widget::Text<'a> {
|
||||
text(content.to_string())
|
||||
.font(Font::MONOSPACE)
|
||||
.size(12)
|
||||
.size(S)
|
||||
.color(MUTED)
|
||||
}
|
||||
|
||||
fn accent<'a>(content: impl std::fmt::Display) -> iced::widget::Text<'a> {
|
||||
text(content.to_string())
|
||||
.font(Font::MONOSPACE)
|
||||
.size(12)
|
||||
.size(S)
|
||||
.color(ACCENT)
|
||||
}
|
||||
|
||||
|
|
@ -74,33 +79,33 @@ fn cell<'a>(content: iced::widget::Text<'a>, width: f32) -> Element<'a, Message>
|
|||
}
|
||||
|
||||
fn action<'a>(label: &str, msg: Message) -> Element<'a, Message> {
|
||||
button(text(label.to_string()).size(12))
|
||||
button(text(label.to_string()).size(S))
|
||||
.on_press(msg)
|
||||
.style(theme::accent())
|
||||
.padding([3, 10])
|
||||
.padding([4, 12])
|
||||
.into()
|
||||
}
|
||||
|
||||
fn action_disabled<'a>(label: &str) -> Element<'a, Message> {
|
||||
button(text(label.to_string()).size(12))
|
||||
.padding([3, 10])
|
||||
button(text(label.to_string()).size(S))
|
||||
.padding([4, 12])
|
||||
.style(theme::quiet())
|
||||
.into()
|
||||
}
|
||||
|
||||
fn quiet_action<'a>(label: &str, msg: Message) -> Element<'a, Message> {
|
||||
button(text(label.to_string()).size(12))
|
||||
button(text(label.to_string()).size(S))
|
||||
.on_press(msg)
|
||||
.style(theme::quiet())
|
||||
.padding([3, 10])
|
||||
.padding([4, 12])
|
||||
.into()
|
||||
}
|
||||
|
||||
fn danger_action<'a>(label: &str, msg: Message) -> Element<'a, Message> {
|
||||
button(text(label.to_string()).size(12))
|
||||
button(text(label.to_string()).size(S))
|
||||
.on_press(msg)
|
||||
.style(theme::danger())
|
||||
.padding([3, 10])
|
||||
.padding([4, 12])
|
||||
.into()
|
||||
}
|
||||
|
||||
|
|
@ -110,11 +115,11 @@ fn tab<'a>(label: impl std::fmt::Display, active: bool, msg: Message) -> Element
|
|||
button(
|
||||
text(label)
|
||||
.font(Font::MONOSPACE)
|
||||
.size(12)
|
||||
.size(S)
|
||||
.color(if active { ACCENT } else { MUTED }),
|
||||
)
|
||||
.on_press(msg)
|
||||
.padding([3, 6])
|
||||
.padding([4, 8])
|
||||
.into()
|
||||
}
|
||||
|
||||
|
|
@ -208,7 +213,7 @@ fn status<'a>(app: &'a Carrier) -> Element<'a, Message> {
|
|||
}
|
||||
(None, Some(status)) => {
|
||||
let piece = if app.status_error {
|
||||
text(status.clone()).font(Font::MONOSPACE).size(12)
|
||||
text(status.clone()).font(Font::MONOSPACE).size(S)
|
||||
} else {
|
||||
muted(status.clone())
|
||||
};
|
||||
|
|
@ -289,22 +294,14 @@ fn list_pane<'a>(app: &'a Carrier) -> Element<'a, Message> {
|
|||
.get(app.folder as usize)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let header = row![
|
||||
cell(muted("date"), DATE_W),
|
||||
cell(muted("f"), FLAG_W),
|
||||
cell(muted("from/to"), FROM_W),
|
||||
muted("subject"),
|
||||
]
|
||||
.spacing(10)
|
||||
.padding(padding());
|
||||
let mut list = column![header].spacing(1);
|
||||
let mut list = column![].spacing(2);
|
||||
if rows.is_empty() {
|
||||
let hint = match app.folder {
|
||||
Folder::Inbox => "inbox is empty — fetch to check the mailbox",
|
||||
Folder::Requests => "no requests",
|
||||
Folder::Sent => "nothing sent yet",
|
||||
};
|
||||
list = list.push(container(muted(hint)).padding(Padding::new(10.0)));
|
||||
list = list.push(container(muted(hint)).padding(Padding::new(14.0)));
|
||||
}
|
||||
for row in &rows {
|
||||
let selected = app
|
||||
|
|
@ -316,43 +313,44 @@ fn list_pane<'a>(app: &'a Carrier) -> Element<'a, Message> {
|
|||
scrollable(list).height(Length::Fill).into()
|
||||
}
|
||||
|
||||
/// One compact row: date, flags, from/to, subject — a flat button so the
|
||||
/// whole line is clickable like a mu4e header.
|
||||
/// One row, like a note title: unread flag and subject on the left, sender
|
||||
/// and date muted on the right — no column chrome, the whole line clicks.
|
||||
fn list_row<'a>(folder: Folder, row: &Row, selected: bool) -> Element<'a, Message> {
|
||||
let flag = if row.unread {
|
||||
accent("●")
|
||||
} else {
|
||||
muted("·")
|
||||
};
|
||||
let date = ops::when(row.at);
|
||||
let subject = if row.subject.is_empty() {
|
||||
"(no subject)".to_string()
|
||||
} else {
|
||||
truncate(&row.subject, 56)
|
||||
};
|
||||
button(
|
||||
row![
|
||||
cell(mono(date), DATE_W),
|
||||
cell(flag, FLAG_W),
|
||||
cell(mono(truncate(&row.from, 32)), FROM_W),
|
||||
text(truncate(&row.subject, 64))
|
||||
flag,
|
||||
text(subject)
|
||||
.font(Font::MONOSPACE)
|
||||
.size(12)
|
||||
.size(S)
|
||||
.color(if row.unread { TEXT } else { MUTED })
|
||||
.wrapping(Wrapping::None),
|
||||
Space::with_width(Length::Fill),
|
||||
muted(truncate(&row.from, 24)),
|
||||
muted(ops::when(row.at)),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(Alignment::Center),
|
||||
)
|
||||
.on_press(Message::MessageSelected(folder, row.id.clone()))
|
||||
.style(theme::row_style(selected))
|
||||
.padding(padding())
|
||||
.padding(Padding::new(4.0).left(12.0).right(12.0))
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn padding() -> Padding {
|
||||
Padding::new(3.0).right(10.0).left(10.0)
|
||||
}
|
||||
|
||||
/// A `label: value` header line for the article view.
|
||||
/// A `label: value` line for the settings page.
|
||||
fn header_line<'a>(label: &str, value: impl std::fmt::Display) -> Element<'a, Message> {
|
||||
row![muted(format!("{label:<9}")), mono(value),]
|
||||
row![muted(format!("{label:<12}")), mono(value),]
|
||||
.spacing(4)
|
||||
.into()
|
||||
}
|
||||
|
|
@ -363,26 +361,22 @@ fn detail_pane<'a>(app: &'a Carrier) -> Element<'a, Message> {
|
|||
.center(Length::Fill)
|
||||
.into();
|
||||
};
|
||||
let mut headers = column![].spacing(1);
|
||||
let to_label = if *folder == Folder::Sent {
|
||||
// One muted meta line carries what the pane used to stack as headers.
|
||||
let label = if *folder == Folder::Sent {
|
||||
"to"
|
||||
} else {
|
||||
"from"
|
||||
};
|
||||
headers = headers.push(header_line(to_label, &msg.from));
|
||||
headers = headers.push(header_line(
|
||||
"subject",
|
||||
if msg.subject.is_empty() {
|
||||
"(no subject)".to_string()
|
||||
} else {
|
||||
msg.subject.clone()
|
||||
},
|
||||
));
|
||||
headers = headers.push(header_line("date", format!("{} UTC", ops::when(msg.time))));
|
||||
headers = headers.push(header_line("id", short(&msg.id, 16)));
|
||||
let mut meta = format!("{label} {} · {} UTC", msg.from, ops::when(msg.time));
|
||||
meta.push_str(&format!(" · id {}", short(&msg.id, 12)));
|
||||
if msg.kept {
|
||||
headers = headers.push(header_line("server", "a copy still exists there"));
|
||||
meta.push_str(" · a server copy still exists");
|
||||
}
|
||||
let subject = if msg.subject.is_empty() {
|
||||
"(no subject)".to_string()
|
||||
} else {
|
||||
msg.subject.clone()
|
||||
};
|
||||
let mut actions = row![].spacing(8);
|
||||
actions = actions.push(quiet_action("reply", Message::ReplyPressed));
|
||||
if *folder == Folder::Requests {
|
||||
|
|
@ -395,18 +389,29 @@ fn detail_pane<'a>(app: &'a Carrier) -> Element<'a, Message> {
|
|||
}
|
||||
actions = actions.push(danger_action("delete", Message::DeletePressed));
|
||||
column![
|
||||
container(headers).padding([8, 10]),
|
||||
container(
|
||||
column![
|
||||
text(subject)
|
||||
.font(Font::MONOSPACE)
|
||||
.size(BIG)
|
||||
.color(TEXT)
|
||||
.wrapping(Wrapping::None),
|
||||
muted(meta),
|
||||
]
|
||||
.spacing(4),
|
||||
)
|
||||
.padding([10, 12]),
|
||||
rule::Rule::horizontal(1.0),
|
||||
scrollable(column![container(
|
||||
text(msg.body.clone())
|
||||
.font(Font::MONOSPACE)
|
||||
.size(12)
|
||||
.size(BODY)
|
||||
.color(TEXT)
|
||||
)
|
||||
.padding([8, 10])])
|
||||
.padding([10, 12])])
|
||||
.height(Length::Fill),
|
||||
rule::Rule::horizontal(1.0),
|
||||
container(actions).padding([6, 10]),
|
||||
container(actions).padding([6, 12]),
|
||||
]
|
||||
.spacing(0)
|
||||
.height(Length::Fill)
|
||||
|
|
@ -430,26 +435,18 @@ fn compose<'a>(app: &'a Carrier) -> Element<'a, Message> {
|
|||
top_bar(app),
|
||||
container(
|
||||
column![
|
||||
row![
|
||||
muted("to"),
|
||||
text_input("alice@example.org or smol://…", &compose.to)
|
||||
.on_input(Message::ComposeToEdited)
|
||||
.size(12)
|
||||
.width(Length::Fill),
|
||||
]
|
||||
.spacing(8),
|
||||
row![
|
||||
muted("subject"),
|
||||
text_input("subject", &compose.subject)
|
||||
.on_input(Message::ComposeSubjectEdited)
|
||||
.size(12)
|
||||
.width(Length::Fill),
|
||||
]
|
||||
.spacing(8),
|
||||
text_input("to — alice@example.org or smol://…", &compose.to)
|
||||
.on_input(Message::ComposeToEdited)
|
||||
.size(S)
|
||||
.width(Length::Fill),
|
||||
text_input("subject", &compose.subject)
|
||||
.on_input(Message::ComposeSubjectEdited)
|
||||
.size(S)
|
||||
.width(Length::Fill),
|
||||
text_editor(&compose.body)
|
||||
.on_action(Message::ComposeBody)
|
||||
.font(Font::MONOSPACE)
|
||||
.size(12)
|
||||
.size(BODY)
|
||||
.height(Length::Fill),
|
||||
row![
|
||||
action(label, Message::SendPressed),
|
||||
|
|
@ -457,9 +454,9 @@ fn compose<'a>(app: &'a Carrier) -> Element<'a, Message> {
|
|||
]
|
||||
.spacing(8),
|
||||
]
|
||||
.spacing(6)
|
||||
.spacing(8)
|
||||
)
|
||||
.padding(10)
|
||||
.padding(12)
|
||||
.height(Length::Fill),
|
||||
]
|
||||
.spacing(0)
|
||||
|
|
@ -491,7 +488,7 @@ fn contacts<'a>(app: &'a Carrier) -> Element<'a, Message> {
|
|||
muted(""),
|
||||
]
|
||||
.spacing(10)
|
||||
.padding(padding());
|
||||
.padding(Padding::new(4.0).left(12.0).right(12.0));
|
||||
let mut table = column![header].spacing(1);
|
||||
for contact in &snap.contacts {
|
||||
table = table.push(contact_row(contact));
|
||||
|
|
@ -510,7 +507,7 @@ fn contacts<'a>(app: &'a Carrier) -> Element<'a, Message> {
|
|||
text_input("paste a smol:// address to import", &app.contact_import)
|
||||
.on_input(Message::ContactImportEdited)
|
||||
.on_submit(Message::ContactImportPressed)
|
||||
.size(12)
|
||||
.size(S)
|
||||
.width(Length::Fill),
|
||||
action("import", Message::ContactImportPressed),
|
||||
]
|
||||
|
|
@ -572,7 +569,7 @@ fn contact_row<'a>(contact: &Contact) -> Element<'a, Message> {
|
|||
.align_y(Alignment::Center),
|
||||
)
|
||||
.style(theme::card)
|
||||
.padding(padding())
|
||||
.padding(Padding::new(4.0).left(12.0).right(12.0))
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
|
@ -631,7 +628,7 @@ fn settings<'a>(app: &'a Carrier) -> Element<'a, Message> {
|
|||
text_editor(&app.import_body)
|
||||
.on_action(Message::ImportBody)
|
||||
.font(Font::MONOSPACE)
|
||||
.size(12)
|
||||
.size(S)
|
||||
.height(Length::Fixed(100.0)),
|
||||
);
|
||||
export = export.push(action("import", Message::ImportPressed));
|
||||
|
|
@ -646,7 +643,7 @@ fn settings<'a>(app: &'a Carrier) -> Element<'a, Message> {
|
|||
muted("timeout"),
|
||||
text_input("30", &app.timeout_text)
|
||||
.on_input(Message::TimeoutEdited)
|
||||
.size(12)
|
||||
.size(S)
|
||||
.width(80),
|
||||
muted("seconds"),
|
||||
]
|
||||
|
|
@ -708,7 +705,7 @@ fn onboard<'a>(app: &'a Carrier) -> Element<'a, Message> {
|
|||
container(
|
||||
text_input("", &app.onboard.seed_shown)
|
||||
.font(Font::MONOSPACE)
|
||||
.size(12)
|
||||
.size(S)
|
||||
.width(Length::Fill)
|
||||
)
|
||||
.style(theme::card)
|
||||
|
|
@ -725,7 +722,7 @@ fn onboard<'a>(app: &'a Carrier) -> Element<'a, Message> {
|
|||
text_input("seed", &app.onboard.seed)
|
||||
.on_input(Message::OnboardSeedEdited)
|
||||
.on_submit(Message::OnboardRestoreFromSeed)
|
||||
.size(12)
|
||||
.size(S)
|
||||
.width(Length::Fill),
|
||||
row![
|
||||
action("restore", Message::OnboardRestoreFromSeed),
|
||||
|
|
@ -741,38 +738,25 @@ fn onboard<'a>(app: &'a Carrier) -> Element<'a, Message> {
|
|||
fn signup<'a>(app: &'a Carrier) -> Element<'a, Message> {
|
||||
let mut body = column![
|
||||
accent("set up the address"),
|
||||
row![
|
||||
muted("user"),
|
||||
text_input("alice", &app.signup.user)
|
||||
.on_input(Message::SignupUserEdited)
|
||||
.size(12)
|
||||
.width(Length::Fill),
|
||||
]
|
||||
.spacing(8),
|
||||
row![
|
||||
muted("host"),
|
||||
text_input("example.org", &app.signup.host)
|
||||
.on_input(Message::SignupHostEdited)
|
||||
.size(12)
|
||||
.width(Length::Fill),
|
||||
]
|
||||
.spacing(8),
|
||||
row![
|
||||
muted("invite"),
|
||||
text_input("optional token", &app.signup.invite)
|
||||
.on_input(Message::SignupInviteEdited)
|
||||
.size(12)
|
||||
.width(Length::Fill),
|
||||
]
|
||||
.spacing(8),
|
||||
row![
|
||||
muted("key"),
|
||||
text_input("server key, if the operator published one", &app.signup.key)
|
||||
.on_input(Message::SignupKeyEdited)
|
||||
.size(12)
|
||||
.width(Length::Fill),
|
||||
]
|
||||
.spacing(8),
|
||||
text_input("user — alice", &app.signup.user)
|
||||
.on_input(Message::SignupUserEdited)
|
||||
.size(S)
|
||||
.width(Length::Fill),
|
||||
text_input("host — example.org", &app.signup.host)
|
||||
.on_input(Message::SignupHostEdited)
|
||||
.size(S)
|
||||
.width(Length::Fill),
|
||||
text_input(
|
||||
"invite token, if the server requires one",
|
||||
&app.signup.invite
|
||||
)
|
||||
.on_input(Message::SignupInviteEdited)
|
||||
.size(S)
|
||||
.width(Length::Fill),
|
||||
text_input("server key, if the operator published one", &app.signup.key,)
|
||||
.on_input(Message::SignupKeyEdited)
|
||||
.size(S)
|
||||
.width(Length::Fill),
|
||||
]
|
||||
.spacing(8);
|
||||
if app.signup.key.is_empty() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue