Files
mail/ui/components/composer-fields.slint
T

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;
}
}