mirror of
https://github.com/flectar/mail.git
synced 2026-11-11 17:57:54 +00:00
523 lines
22 KiB
Plaintext
523 lines
22 KiB
Plaintext
import { HorizontalBox } from "std-widgets.slint";
|
|
import { AccountRow, ComposeContact } from "../models.slint";
|
|
import { Avatar } from "avatar.slint";
|
|
import { IconButton } from "controls.slint";
|
|
import { ModeButton, RecipientRow } from "composer-controls.slint";
|
|
import { ComposerContactMenu } from "composer-contact-menu.slint";
|
|
import { TypeScale } from "../typography.slint";
|
|
import { WindowControlsClearance } from "../window-chrome.slint";
|
|
|
|
// What a composer is for: its heading, and its window's title.
|
|
export global ComposerText {
|
|
public pure function mode-title(mode: string) -> string {
|
|
return mode == "reply" ? @tr("Reply")
|
|
: mode == "reply_all" ? @tr("Reply all")
|
|
: mode == "forward" ? @tr("Forward")
|
|
: mode == "draft" ? @tr("Edit draft")
|
|
: mode == "edit_as_new" ? @tr("Edit as new")
|
|
: @tr("New message");
|
|
}
|
|
}
|
|
|
|
export component ComposerHeader inherits Rectangle {
|
|
in property <bool> compact: false;
|
|
in property <string> mode: "new";
|
|
// On the desktop: move the message into a window of its own.
|
|
in property <bool> can-pop-out: false;
|
|
in property <color> control-hover-color;
|
|
in property <color> muted-color;
|
|
in property <color> raised-color;
|
|
in property <color> secondary-color;
|
|
in property <color> subtle-border-color;
|
|
in property <color> title-color;
|
|
callback request-close();
|
|
callback pop-out();
|
|
|
|
height: 52px;
|
|
background: root.raised-color;
|
|
|
|
// The full-window compact composer can sit under the window controls.
|
|
header-clearance := WindowControlsClearance { width: parent.width; height: parent.height; }
|
|
|
|
Rectangle {
|
|
x: 0px;
|
|
y: parent.height - 1px;
|
|
width: parent.width;
|
|
height: 1px;
|
|
background: root.subtle-border-color;
|
|
}
|
|
|
|
Text {
|
|
x: max(18px, header-clearance.leading);
|
|
width: parent.width - self.x - (root.can-pop-out ? 142px : 102px) - header-clearance.trailing;
|
|
height: parent.height;
|
|
text: ComposerText.mode-title(root.mode);
|
|
color: root.title-color;
|
|
font-size: TypeScale.heading-small;
|
|
font-weight: 700;
|
|
vertical-alignment: center;
|
|
}
|
|
|
|
if root.can-pop-out : IconButton {
|
|
x: parent.width - (root.compact ? 100px : 78px) - header-clearance.trailing;
|
|
y: root.compact ? 4px : 10px;
|
|
width: root.compact ? 44px : 32px;
|
|
height: root.compact ? 44px : 32px;
|
|
icon: @image-url("../icons/lucide/outline/external-link.svg");
|
|
icon-color: root.secondary-color;
|
|
hover-background: root.control-hover-color;
|
|
tooltip-text: @tr("Open in a new window");
|
|
clicked => { root.pop-out(); }
|
|
}
|
|
|
|
IconButton {
|
|
x: parent.width - (root.compact ? 52px : 42px) - header-clearance.trailing;
|
|
y: root.compact ? 4px : 10px;
|
|
width: root.compact ? 44px : 32px;
|
|
height: root.compact ? 44px : 32px;
|
|
icon: @image-url("../icons/lucide/outline/x.svg");
|
|
icon-color: root.secondary-color;
|
|
hover-background: root.control-hover-color;
|
|
tooltip-text: @tr("Close");
|
|
clicked => { root.request-close(); }
|
|
}
|
|
}
|
|
|
|
export component ComposerFromRow inherits Rectangle {
|
|
in property <[AccountRow]> accounts: [];
|
|
in-out property <int> selected-account-id: -1;
|
|
in-out property <string> from-label: @tr("No connected account");
|
|
in property <color> background-color;
|
|
in property <color> frame-border-color;
|
|
in property <color> hover-color;
|
|
in property <color> muted-color;
|
|
in property <color> secondary-color;
|
|
in property <color> selected-background;
|
|
in property <color> title-color;
|
|
callback changed(int);
|
|
// Account rows hold a name and an address that grow with the platform font.
|
|
private property <length> account-pitch: 14px + max(18px, account-name-probe.preferred-height)
|
|
+ max(16px, account-email-probe.preferred-height);
|
|
|
|
private property <int> highlighted-index: 0;
|
|
|
|
function choose(index: int) {
|
|
if (index < 0 || index >= root.accounts.length) {
|
|
return;
|
|
}
|
|
root.selected-account-id = root.accounts[index].id;
|
|
root.from-label = root.accounts[index].name == root.accounts[index].email
|
|
? root.accounts[index].email
|
|
: root.accounts[index].name + " <" + root.accounts[index].email + ">";
|
|
root.changed(root.selected-account-id);
|
|
account-popup.close();
|
|
}
|
|
|
|
height: 42px;
|
|
background: root.background-color;
|
|
|
|
account-label := Text {
|
|
x: 12px;
|
|
width: max(60px, self.preferred-width);
|
|
height: parent.height;
|
|
text: @tr("Account");
|
|
color: root.muted-color;
|
|
font-size: TypeScale.caption;
|
|
font-weight: 600;
|
|
vertical-alignment: center;
|
|
}
|
|
|
|
trigger := Rectangle {
|
|
x: account-label.x + account-label.width + 4px;
|
|
y: 5px;
|
|
width: min(410px, parent.width - self.x - 12px);
|
|
height: 32px;
|
|
border-radius: 7px;
|
|
border-width: 1px;
|
|
border-color: account-popup.is-open ? root.secondary-color : root.frame-border-color;
|
|
background: account-hit.has-hover ? root.hover-color : root.background-color;
|
|
|
|
Text {
|
|
x: 10px;
|
|
width: parent.width - 36px;
|
|
height: parent.height;
|
|
text: root.from-label;
|
|
color: root.accounts.length > 0 ? root.title-color : root.muted-color;
|
|
font-size: TypeScale.caption;
|
|
font-weight: root.accounts.length > 0 ? 600 : 500;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
|
|
Image {
|
|
x: parent.width - 25px;
|
|
y: 9px;
|
|
width: 14px;
|
|
height: 14px;
|
|
source: @image-url("../icons/lucide/outline/chevron-down.svg");
|
|
colorize: root.muted-color;
|
|
image-fit: contain;
|
|
transform-rotation: account-popup.is-open ? 180deg : 0deg;
|
|
accessible-role: none;
|
|
animate transform-rotation { duration: 120ms; }
|
|
}
|
|
|
|
account-focus := FocusScope {
|
|
key-pressed(event) => {
|
|
if (event.text == Key.Return || event.text == Key.Space || event.text == Key.DownArrow) {
|
|
if (root.accounts.length > 0) {
|
|
account-popup.show();
|
|
}
|
|
return accept;
|
|
}
|
|
if (event.text == Key.Escape) {
|
|
account-popup.close();
|
|
return accept;
|
|
}
|
|
reject
|
|
}
|
|
|
|
account-hit := TouchArea {
|
|
enabled: root.accounts.length > 0;
|
|
clicked => {
|
|
account-focus.focus();
|
|
root.highlighted-index = 0;
|
|
account-popup.show();
|
|
}
|
|
mouse-cursor: root.accounts.length > 0 ? pointer : default;
|
|
}
|
|
}
|
|
}
|
|
|
|
account-name-probe := Text { visible: false; text: "Ag"; font-size: TypeScale.secondary; font-weight: 600; }
|
|
account-email-probe := Text { visible: false; text: "Ag"; font-size: TypeScale.caption; }
|
|
|
|
account-popup := PopupWindow {
|
|
x: 50px;
|
|
y: 38px;
|
|
width: min(410px, root.width - 62px);
|
|
height: max(52px, min(5, root.accounts.length) * root.account-pitch + 8px);
|
|
close-policy: close-on-click-outside;
|
|
forward-focus: account-list;
|
|
|
|
popup-frame := Rectangle {
|
|
border-radius: 8px;
|
|
border-width: 1px;
|
|
border-color: root.frame-border-color;
|
|
background: root.background-color;
|
|
drop-shadow-blur: 18px;
|
|
drop-shadow-offset-y: 5px;
|
|
drop-shadow-color: #0000002e;
|
|
clip: true;
|
|
|
|
account-list := FocusScope {
|
|
key-pressed(event) => {
|
|
if (event.text == Key.DownArrow) {
|
|
root.highlighted-index = min(root.accounts.length - 1, root.highlighted-index + 1);
|
|
return accept;
|
|
}
|
|
if (event.text == Key.UpArrow) {
|
|
root.highlighted-index = max(0, root.highlighted-index - 1);
|
|
return accept;
|
|
}
|
|
if (event.text == Key.Return || event.text == Key.Space) {
|
|
root.choose(root.highlighted-index);
|
|
return accept;
|
|
}
|
|
if (event.text == Key.Escape) {
|
|
account-popup.close();
|
|
return accept;
|
|
}
|
|
reject
|
|
}
|
|
|
|
for account[index] in root.accounts : Rectangle {
|
|
x: 4px;
|
|
y: 4px + index * root.account-pitch;
|
|
width: parent.width - 8px;
|
|
height: root.account-pitch - 4px;
|
|
border-radius: 6px;
|
|
background: index == root.highlighted-index || account-row-hit.has-hover
|
|
? root.selected-background : transparent;
|
|
|
|
Avatar {
|
|
x: 8px;
|
|
y: (parent.height - self.height) / 2;
|
|
size: 26px;
|
|
source: account.avatar_small;
|
|
has-source: account.has_avatar;
|
|
fallback: account.initials;
|
|
background-color: account.has_avatar ? #ffffff : root.selected-background;
|
|
foreground-color: root.title-color;
|
|
avatar-border-width: account.has_avatar ? 1px : 0px;
|
|
avatar-border-color: root.frame-border-color;
|
|
}
|
|
|
|
account-name := Text {
|
|
x: 43px;
|
|
y: 5px;
|
|
width: parent.width - 76px;
|
|
height: max(18px, self.preferred-height);
|
|
text: account.name;
|
|
color: root.title-color;
|
|
font-size: TypeScale.secondary;
|
|
font-weight: 600;
|
|
overflow: elide;
|
|
}
|
|
|
|
Text {
|
|
x: 43px;
|
|
y: account-name.y + account-name.height - 1px;
|
|
width: parent.width - 76px;
|
|
height: max(16px, self.preferred-height);
|
|
text: account.email;
|
|
color: root.muted-color;
|
|
font-size: TypeScale.caption;
|
|
overflow: elide;
|
|
}
|
|
|
|
Image {
|
|
x: parent.width - 25px;
|
|
y: (parent.height - self.height) / 2;
|
|
width: 16px;
|
|
height: 16px;
|
|
visible: account.id == root.selected-account-id;
|
|
source: @image-url("../icons/lucide/outline/check.svg");
|
|
colorize: root.secondary-color;
|
|
accessible-role: none;
|
|
}
|
|
|
|
account-row-hit := TouchArea {
|
|
clicked => { root.choose(index); }
|
|
mouse-cursor: pointer;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export component ComposerRecipients inherits Rectangle {
|
|
in property <bool> open: false;
|
|
in-out property <string> to;
|
|
in-out property <string> cc;
|
|
in-out property <string> bcc;
|
|
in property <[ComposeContact]> contacts: [];
|
|
in property <color> accent-background;
|
|
in property <color> accent-color;
|
|
in property <color> background-color;
|
|
in property <color> body-color;
|
|
in property <color> control-hover-color;
|
|
in property <color> muted-color;
|
|
in property <color> raised-color;
|
|
in property <color> subtle-border-color;
|
|
in property <color> title-color;
|
|
out property <bool> contact-menu-open: contact-popup.is-open && root.contacts.length > 0;
|
|
in-out property <int> highlighted-contact: 0;
|
|
callback search-contacts(string);
|
|
callback contact-selected(int);
|
|
private property <bool> show-cc: root.cc != "";
|
|
private property <bool> show-bcc: root.bcc != "";
|
|
// Recipients filled in for the user, such as from a mail link, always
|
|
// show, even in a row that was toggled off.
|
|
changed cc => { if (root.cc != "") { root.show-cc = true; } }
|
|
changed bcc => { if (root.bcc != "") { root.show-bcc = true; } }
|
|
private property <bool> suggestions-dismissed: false;
|
|
// Suggestion rows hold a name and an address that grow with the platform font.
|
|
private property <length> suggestion-pitch: 13px + max(18px, suggestion-name-probe.preferred-height)
|
|
+ max(15px, suggestion-email-probe.preferred-height);
|
|
|
|
function move-contact-highlight(delta: int) {
|
|
if (root.contacts.length == 0) {
|
|
return;
|
|
}
|
|
root.highlighted-contact = max(0, min(root.contacts.length - 1, root.highlighted-contact + delta));
|
|
}
|
|
|
|
function choose-highlighted-contact() {
|
|
if (root.contacts.length > 0) {
|
|
root.contact-selected(root.highlighted-contact);
|
|
root.suggestions-dismissed = true;
|
|
contact-popup.close();
|
|
to-row.focus();
|
|
}
|
|
}
|
|
|
|
changed open => {
|
|
if root.open {
|
|
// A message that already has recipients, such as a reply, a
|
|
// draft or one moved into its own window, goes on in its body.
|
|
if root.to == "" {
|
|
to-row.focus();
|
|
}
|
|
} else {
|
|
contact-popup.close();
|
|
}
|
|
}
|
|
|
|
height: recipient-rows.preferred-height;
|
|
background: root.background-color;
|
|
|
|
recipient-rows := VerticalLayout {
|
|
to-row := RecipientRow {
|
|
label: @tr("To");
|
|
placeholder-text: @tr("name@example.com · separate with commas");
|
|
right-padding: parent.width - recipient-modes.x + 8px;
|
|
text <=> root.to;
|
|
background-color: root.background-color;
|
|
label-color: root.muted-color;
|
|
text-color: root.body-color;
|
|
placeholder-color: root.muted-color;
|
|
focus-color: root.accent-color;
|
|
suggestions-open: root.contact-menu-open;
|
|
edited(value) => {
|
|
root.suggestions-dismissed = false;
|
|
root.highlighted-contact = 0;
|
|
root.search-contacts(value);
|
|
if (root.contacts.length > 0) {
|
|
contact-popup.show();
|
|
to-row.focus();
|
|
} else {
|
|
contact-popup.close();
|
|
}
|
|
}
|
|
move-suggestion(delta) => { root.move-contact-highlight(delta); }
|
|
accept-suggestion => { root.choose-highlighted-contact(); }
|
|
dismiss-suggestions => { root.suggestions-dismissed = true; }
|
|
}
|
|
|
|
if root.show-cc : RecipientRow {
|
|
label: @tr("Cc");
|
|
placeholder-text: @tr("Carbon copy");
|
|
text <=> root.cc;
|
|
background-color: root.background-color;
|
|
label-color: root.muted-color;
|
|
text-color: root.body-color;
|
|
placeholder-color: root.muted-color;
|
|
focus-color: root.accent-color;
|
|
}
|
|
|
|
if root.show-bcc : RecipientRow {
|
|
label: @tr("Bcc");
|
|
placeholder-text: @tr("Blind carbon copy");
|
|
text <=> root.bcc;
|
|
background-color: root.background-color;
|
|
label-color: root.muted-color;
|
|
text-color: root.body-color;
|
|
placeholder-color: root.muted-color;
|
|
focus-color: root.accent-color;
|
|
}
|
|
}
|
|
|
|
recipient-modes := HorizontalBox {
|
|
x: parent.width - self.width - 10px;
|
|
y: 7px;
|
|
width: cc-mode.width + bcc-mode.width + 2px;
|
|
height: 26px;
|
|
spacing: 2px;
|
|
|
|
cc-mode := ModeButton {
|
|
width: max(36px, self.label-width + 12px);
|
|
text: @tr("Cc");
|
|
selected: root.show-cc;
|
|
text-color: root.muted-color;
|
|
selected-text-color: root.accent-color;
|
|
hover-color: root.control-hover-color;
|
|
selected-background: root.accent-background;
|
|
clicked => { root.show-cc = !root.show-cc; }
|
|
}
|
|
bcc-mode := ModeButton {
|
|
width: max(38px, self.label-width + 12px);
|
|
text: @tr("Bcc");
|
|
selected: root.show-bcc;
|
|
text-color: root.muted-color;
|
|
selected-text-color: root.accent-color;
|
|
hover-color: root.control-hover-color;
|
|
selected-background: root.accent-background;
|
|
clicked => { root.show-bcc = !root.show-bcc; }
|
|
}
|
|
}
|
|
|
|
suggestion-name-probe := Text { visible: false; text: "Ag"; font-size: TypeScale.secondary; font-weight: 600; }
|
|
suggestion-email-probe := Text { visible: false; text: "Ag"; font-size: TypeScale.caption; }
|
|
|
|
contact-popup := PopupWindow {
|
|
x: 44px;
|
|
y: to-row.height - 3px;
|
|
width: min(430px, root.width - 56px);
|
|
height: min(6, root.contacts.length) * root.suggestion-pitch + 8px;
|
|
close-policy: close-on-click-outside;
|
|
forward-focus: to-row;
|
|
|
|
ComposerContactMenu {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
row-pitch: root.suggestion-pitch;
|
|
contacts: root.contacts;
|
|
highlighted-contact: root.highlighted-contact;
|
|
accent-background: root.accent-background;
|
|
accent-color: root.accent-color;
|
|
background-color: root.background-color;
|
|
frame-border-color: root.subtle-border-color;
|
|
hover-color: root.raised-color;
|
|
muted-color: root.muted-color;
|
|
title-color: root.title-color;
|
|
selected(index) => {
|
|
root.contact-selected(index);
|
|
root.suggestions-dismissed = true;
|
|
contact-popup.close();
|
|
to-row.focus();
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
export component ComposerSubject inherits Rectangle {
|
|
in-out property <string> subject;
|
|
in property <color> accent-color;
|
|
in property <color> background-color;
|
|
in property <color> muted-color;
|
|
in property <color> title-color;
|
|
|
|
height: 42px;
|
|
background: root.background-color;
|
|
|
|
subject-label := Text {
|
|
x: 12px;
|
|
width: max(46px, self.preferred-width);
|
|
height: parent.height;
|
|
text: @tr("Subject");
|
|
color: subject-input.has-focus ? root.accent-color : root.muted-color;
|
|
font-size: TypeScale.caption;
|
|
font-weight: 600;
|
|
vertical-alignment: center;
|
|
}
|
|
|
|
Text {
|
|
x: subject-label.x + subject-label.width + 12px;
|
|
width: parent.width - self.x - 12px;
|
|
height: parent.height;
|
|
visible: root.subject == "";
|
|
text: @tr("What is this email about?");
|
|
color: root.muted-color;
|
|
font-size: TypeScale.body;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
|
|
subject-input := TextInput {
|
|
x: subject-label.x + subject-label.width + 12px;
|
|
width: parent.width - self.x - 12px;
|
|
height: parent.height;
|
|
text <=> root.subject;
|
|
color: root.title-color;
|
|
selection-background-color: root.accent-color;
|
|
selection-foreground-color: white;
|
|
font-size: TypeScale.body-large;
|
|
font-weight: 600;
|
|
single-line: true;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|