mirror of
https://github.com/flectar/mail.git
synced 2026-11-11 17:57:54 +00:00
306 lines
12 KiB
Plaintext
306 lines
12 KiB
Plaintext
import { ScrollView } from "std-widgets.slint";
|
|
import { TypeScale } from "../typography.slint";
|
|
import { AppTheme, LayoutMetrics, MotionSettings, ThemeUtilities } from "../design-system.slint";
|
|
import { MailProfileRow } from "../models.slint";
|
|
import { ProfilePalette } from "profile-controls.slint";
|
|
|
|
// A profile's color as a pill fill that keeps white text readable, darkened
|
|
// no more than it needs.
|
|
export global ProfileColors {
|
|
public pure function fill(value: color) -> color {
|
|
return ThemeUtilities.contrast(white, value) >= 4.5 ? value
|
|
: ThemeUtilities.contrast(white, value.mix(black, 0.88)) >= 4.5 ? value.mix(black, 0.88)
|
|
: value.mix(black, 0.76);
|
|
}
|
|
}
|
|
|
|
// One line of the profile menu: a profile, "All accounts" or an action.
|
|
component ProfileMenuRow inherits Rectangle {
|
|
in property <string> title;
|
|
in property <string> detail;
|
|
// A profile shows its color; the other lines an icon.
|
|
in property <color> dot-color: transparent;
|
|
in property <image> icon;
|
|
in property <bool> checkable;
|
|
in property <bool> checked;
|
|
in property <bool> enabled: true;
|
|
in property <bool> highlighted;
|
|
in property <int> index;
|
|
callback activated;
|
|
|
|
height: 32px;
|
|
border-radius: 6px;
|
|
background: root.enabled && (root.highlighted || hit.has-hover) ? AppTheme.control-bg : transparent;
|
|
|
|
if root.checked : Image {
|
|
x: 8px;
|
|
y: (parent.height - self.height) / 2;
|
|
width: 14px;
|
|
height: 14px;
|
|
source: @image-url("../icons/lucide/outline/check.svg");
|
|
colorize: AppTheme.accent;
|
|
accessible-role: none;
|
|
}
|
|
if root.dot-color != transparent : Rectangle {
|
|
x: 30px;
|
|
y: (parent.height - self.height) / 2;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 5px;
|
|
background: root.dot-color;
|
|
opacity: root.enabled ? 1 : 0.45;
|
|
}
|
|
if root.dot-color == transparent : Image {
|
|
x: 28px;
|
|
y: (parent.height - self.height) / 2;
|
|
width: 14px;
|
|
height: 14px;
|
|
source: root.icon;
|
|
colorize: AppTheme.secondary;
|
|
opacity: root.enabled ? 1 : 0.45;
|
|
accessible-role: none;
|
|
}
|
|
title-label := Text {
|
|
x: 50px;
|
|
width: max(0px, parent.width - self.x - (root.detail == "" ? 10px : detail-label.width + 20px));
|
|
height: parent.height;
|
|
text: root.title;
|
|
color: root.enabled ? AppTheme.heading : AppTheme.muted;
|
|
font-size: TypeScale.secondary;
|
|
font-weight: root.checked ? 600 : 500;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
accessible-role: none;
|
|
}
|
|
detail-label := Text {
|
|
x: parent.width - self.width - 10px;
|
|
width: min(self.preferred-width, parent.width / 2);
|
|
height: parent.height;
|
|
visible: root.detail != "";
|
|
text: root.detail;
|
|
color: AppTheme.muted;
|
|
font-size: TypeScale.caption;
|
|
horizontal-alignment: right;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
accessible-role: none;
|
|
}
|
|
hit := TouchArea {
|
|
enabled: root.enabled;
|
|
mouse-cursor: root.enabled ? pointer : default;
|
|
accessible-role: button;
|
|
accessible-label: root.detail == "" ? root.title : root.title + ", " + root.detail;
|
|
accessible-enabled: root.enabled;
|
|
accessible-checkable: root.checkable;
|
|
accessible-checked: root.checked;
|
|
accessible-item-index: root.index;
|
|
accessible-action-default => { root.activated(); }
|
|
clicked => { root.activated(); }
|
|
}
|
|
}
|
|
|
|
// The profile pill at the start of the tab strip. It names the profile the
|
|
// mail view shows, or "All accounts", and its menu switches profiles or opens
|
|
// the profile editor.
|
|
export component ProfileSwitcher inherits Rectangle {
|
|
in property <[MailProfileRow]> profiles;
|
|
// The profile the mail view shows; an empty id means every account.
|
|
in property <MailProfileRow> active;
|
|
in property <int> account-count;
|
|
in property <length> widest: 168px;
|
|
// Span `widest`, as a sidebar control does, instead of fitting the label.
|
|
in property <bool> fill-width: false;
|
|
callback choose(string);
|
|
callback create-profile();
|
|
callback manage-profiles();
|
|
|
|
private property <bool> all: root.active.id == "";
|
|
private property <color> fill: root.all ? AppTheme.title.with-alpha(0.07) : ProfileColors.fill(root.active.color);
|
|
private property <color> foreground: root.all ? AppTheme.title : ThemeUtilities.contrasting-foreground(root.fill);
|
|
private property <bool> menu-open: menu.is-open;
|
|
// Menu lines: "All accounts", each profile, then the two actions. Past
|
|
// eight profiles, their list scrolls.
|
|
private property <int> line-count: root.profiles.length + 3;
|
|
private property <int> shown-profiles: min(root.profiles.length, 8);
|
|
private property <int> highlighted: -1;
|
|
private property <string> label: root.all ? @tr("All accounts") : root.active.name;
|
|
|
|
function line-enabled(line: int) -> bool {
|
|
return line <= 0 || line > root.profiles.length || root.profiles[line - 1].account_count > 0;
|
|
}
|
|
|
|
function move-highlight(step: int) {
|
|
root.highlighted = root.highlighted < 0 ? (step > 0 ? 0 : root.line-count - 1)
|
|
: mod(root.highlighted + step + root.line-count, root.line-count);
|
|
}
|
|
|
|
function activate(line: int) {
|
|
if !root.line-enabled(line) { return; }
|
|
menu.close();
|
|
if line == 0 {
|
|
root.choose("");
|
|
} else if line <= root.profiles.length {
|
|
root.choose(root.profiles[line - 1].id);
|
|
} else if line == root.profiles.length + 1 {
|
|
root.create-profile();
|
|
} else {
|
|
root.manage-profiles();
|
|
}
|
|
}
|
|
|
|
// A profile's name follows its color dot.
|
|
private property <length> label-x: root.all ? 11px : 28px;
|
|
|
|
width: root.fill-width ? root.widest
|
|
: min(root.widest, root.label-x + label-text.preferred-width + 23px);
|
|
height: 28px;
|
|
// Beside the sidebar's buttons it takes their corners.
|
|
border-radius: root.fill-width ? LayoutMetrics.corner-radius - 2px : 7px;
|
|
background: root.menu-open || hit.pressed ? root.fill.mix(root.foreground, 0.86)
|
|
: hit.has-hover ? root.fill.mix(root.foreground, 0.92) : root.fill;
|
|
animate background { duration: MotionSettings.enabled ? 120ms : 0ms; easing: ease-out; }
|
|
|
|
if !root.all : Rectangle {
|
|
x: 11px;
|
|
y: (parent.height - self.height) / 2;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 5px;
|
|
background: root.foreground;
|
|
opacity: 0.9;
|
|
}
|
|
label-text := Text {
|
|
x: root.label-x;
|
|
width: max(0px, parent.width - self.x - 23px);
|
|
height: parent.height;
|
|
text: root.label;
|
|
color: root.foreground;
|
|
font-size: TypeScale.secondary;
|
|
font-weight: 600;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
accessible-role: none;
|
|
}
|
|
Image {
|
|
x: parent.width - self.width - 8px;
|
|
y: (parent.height - self.height) / 2 + 1px;
|
|
width: 10px;
|
|
height: 10px;
|
|
source: @image-url("../icons/lucide/outline/chevron-down.svg");
|
|
colorize: root.all ? AppTheme.secondary : root.foreground;
|
|
accessible-role: none;
|
|
}
|
|
hit := TouchArea {
|
|
mouse-cursor: pointer;
|
|
accessible-role: button;
|
|
accessible-label: @tr("Profile");
|
|
accessible-value: root.label;
|
|
accessible-expanded: root.menu-open;
|
|
accessible-action-default => { root.toggle-menu(); }
|
|
clicked => { root.toggle-menu(); }
|
|
}
|
|
|
|
function toggle-menu() {
|
|
if root.menu-open {
|
|
menu.close();
|
|
} else {
|
|
root.highlighted = -1;
|
|
menu.show();
|
|
}
|
|
}
|
|
|
|
menu := PopupWindow {
|
|
x: 0px;
|
|
y: root.height + 6px;
|
|
width: 260px;
|
|
// Lines, the separator and the frame's padding.
|
|
height: (root.shown-profiles + 3) * 32px + 9px + 8px;
|
|
close-policy: close-on-click-outside;
|
|
forward-focus: menu-keys;
|
|
|
|
Rectangle {
|
|
border-radius: 8px;
|
|
border-width: 1px;
|
|
border-color: AppTheme.border;
|
|
background: AppTheme.surface-raised;
|
|
drop-shadow-blur: 18px;
|
|
drop-shadow-offset-y: 5px;
|
|
drop-shadow-color: #0000002e;
|
|
accessible-role: groupbox;
|
|
accessible-label: @tr("Profiles");
|
|
|
|
menu-keys := FocusScope {
|
|
capture-key-pressed(event) => {
|
|
if event.text == Key.DownArrow { root.move-highlight(1); return accept; }
|
|
if event.text == Key.UpArrow { root.move-highlight(-1); return accept; }
|
|
if event.text == Key.Return || event.text == Key.Space {
|
|
if root.highlighted >= 0 { root.activate(root.highlighted); }
|
|
return accept;
|
|
}
|
|
if event.text == Key.Escape { menu.close(); return accept; }
|
|
reject
|
|
}
|
|
|
|
VerticalLayout {
|
|
padding: 4px;
|
|
spacing: 0px;
|
|
accessible-role: list;
|
|
accessible-label: @tr("Profiles");
|
|
|
|
ProfileMenuRow {
|
|
title: @tr("All accounts");
|
|
detail: ProfilePalette.count-label(root.account-count);
|
|
checkable: true;
|
|
checked: root.all;
|
|
highlighted: root.highlighted == 0;
|
|
index: 0;
|
|
activated => { root.activate(0); }
|
|
}
|
|
if root.profiles.length > 0 : ScrollView {
|
|
height: root.shown-profiles * 32px;
|
|
content-width: self.visible-width;
|
|
|
|
VerticalLayout {
|
|
for profile[index] in root.profiles : ProfileMenuRow {
|
|
title: profile.name;
|
|
detail: ProfilePalette.count-label(profile.account_count);
|
|
dot-color: profile.color;
|
|
checkable: true;
|
|
checked: profile.id == root.active.id;
|
|
enabled: profile.account_count > 0;
|
|
highlighted: root.highlighted == index + 1;
|
|
index: index + 1;
|
|
activated => { root.activate(index + 1); }
|
|
}
|
|
}
|
|
}
|
|
Rectangle {
|
|
height: 9px;
|
|
Rectangle {
|
|
x: 8px;
|
|
y: 4px;
|
|
width: parent.width - 16px;
|
|
height: 1px;
|
|
background: AppTheme.border-subtle;
|
|
}
|
|
}
|
|
ProfileMenuRow {
|
|
title: @tr("New profile…");
|
|
icon: @image-url("../icons/lucide/outline/plus.svg");
|
|
highlighted: root.highlighted == root.profiles.length + 1;
|
|
index: root.profiles.length + 1;
|
|
activated => { root.activate(root.profiles.length + 1); }
|
|
}
|
|
ProfileMenuRow {
|
|
title: @tr("Edit profiles…");
|
|
icon: @image-url("../icons/lucide/outline/pencil.svg");
|
|
highlighted: root.highlighted == root.profiles.length + 2;
|
|
index: root.profiles.length + 2;
|
|
activated => { root.activate(root.profiles.length + 2); }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|