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 compact: false; in property mode: "new"; // On the desktop: move the message into a window of its own. in property can-pop-out: false; in property control-hover-color; in property muted-color; in property raised-color; in property secondary-color; in property subtle-border-color; in property 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 selected-account-id: -1; in-out property from-label: @tr("No connected account"); in property background-color; in property frame-border-color; in property hover-color; in property muted-color; in property secondary-color; in property selected-background; in property title-color; callback changed(int); // Account rows hold a name and an address that grow with the platform font. private property account-pitch: 14px + max(18px, account-name-probe.preferred-height) + max(16px, account-email-probe.preferred-height); private property 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 open: false; in-out property to; in-out property cc; in-out property bcc; in property <[ComposeContact]> contacts: []; in property accent-background; in property accent-color; in property background-color; in property body-color; in property control-hover-color; in property muted-color; in property raised-color; in property subtle-border-color; in property title-color; out property contact-menu-open: contact-popup.is-open && root.contacts.length > 0; in-out property highlighted-contact: 0; callback search-contacts(string); callback contact-selected(int); private property show-cc: root.cc != ""; private property 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 suggestions-dismissed: false; // Suggestion rows hold a name and an address that grow with the platform font. private property 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 subject; in property accent-color; in property background-color; in property muted-color; in property 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; } }