Files
mail/ui/components/settings-components.slint
T

418 lines
14 KiB
Plaintext

import { ComboBox, HorizontalBox, VerticalBox } from "std-widgets.slint";
import { LineEdit, Switch } from "controls.slint";
import { FocusVisibleScope } from "focus-visible-scope.slint";
import { TypeScale } from "../typography.slint";
import { SidebarStyle } from "sidebar-row.slint";
import { KbdShortcut, KeyboardKeys } from "kbd.slint";
// Settings navigation is intentionally separate from MailSidebar: the latter
// owns mailbox models, compose actions, and tree expansion state.
export component SettingsNavItem inherits Rectangle {
in property <string> text;
in property <image> icon;
in property <bool> selected: false;
in property <color> accent;
in property <color> accent-soft;
in property <color> body-text;
in property <color> muted-text;
in property <color> icon-color: root.selected ? root.accent : root.muted-text;
callback clicked;
height: 36px;
border-radius: 7px;
background: root.selected ? root.accent-soft : nav-hit.has-hover ? root.accent-soft : transparent;
Image {
x: 10px;
y: (parent.height - self.height) / 2;
width: SidebarStyle.icon-size;
height: SidebarStyle.icon-size;
source: root.icon;
colorize: root.icon-color;
image-fit: contain;
accessible-role: none;
}
Text {
x: 37px;
width: parent.width - 47px;
height: parent.height;
text: root.text;
color: root.body-text;
font-size: SidebarStyle.label-size;
font-weight: root.selected ? SidebarStyle.selected-weight : SidebarStyle.normal-weight;
vertical-alignment: center;
overflow: elide;
}
nav-hit := TouchArea {
accessible-role: button;
accessible-label: root.text;
accessible-checked: root.selected;
accessible-action-default => { root.clicked(); }
clicked => { root.clicked(); }
mouse-cursor: pointer;
}
}
export component SettingsCard inherits Rectangle {
in property <string> title;
in property <string> description;
in property <color> surface;
in property <color> card-border-color;
in property <color> heading;
in property <color> secondary-text;
// Where content placed below the header starts. Larger platform fonts
// make the header taller than its 58px design, so callers offset from it.
out property <length> content-y: description-text.y + description-text.height;
background: root.surface;
border-radius: 10px;
border-width: 1px;
border-color: root.card-border-color;
title-text := Text {
x: 20px;
y: 17px;
width: parent.width - 40px;
height: max(20px, self.preferred-height);
text: root.title;
color: root.heading;
font-size: TypeScale.subtitle;
font-weight: 600;
letter-spacing: -0.3px;
vertical-alignment: center;
overflow: elide;
}
description-text := Text {
x: 20px;
y: title-text.y + title-text.height + 4px;
width: parent.width - 40px;
height: max(17px, self.preferred-height);
text: root.description;
color: root.secondary-text;
font-size: TypeScale.caption;
vertical-alignment: center;
overflow: elide;
}
}
// Shared preference row for boolean appearance and behavior settings. Keeping
// the label, description, touch target, and spacing here prevents individual
// settings cards from slowly drifting apart.
export component SettingsSwitchRow inherits Rectangle {
in property <bool> compact: false;
in property <length> compact-title-height: 17px;
in property <length> compact-description-height: 34px;
in property <length> desktop-height: 48px;
in property <string> title;
in property <string> description;
in property <bool> checked: false;
in property <bool> enabled: true;
in property <color> title-color;
in property <color> description-color;
callback toggled(bool);
// Both texts grow past their designed heights with larger platform fonts.
height: root.compact
? 27px + title-text.height + description-text.height
: root.desktop-height + title-text.height + description-text.height - 34px;
opacity: root.enabled ? 1 : 0.5;
HorizontalBox {
spacing: 16px;
VerticalBox {
horizontal-stretch: 1;
min-width: 0px;
spacing: 2px;
title-text := Text {
height: max(root.compact ? root.compact-title-height : 17px, self.preferred-height);
text: root.title;
color: root.title-color;
font-size: TypeScale.body;
font-weight: 600;
wrap: root.compact ? word-wrap : no-wrap;
overflow: root.compact ? clip : elide;
}
description-text := Text {
height: max(root.compact ? root.compact-description-height : 17px, self.preferred-height);
text: root.description;
color: root.description-color;
font-size: TypeScale.caption;
wrap: word-wrap;
}
}
Switch {
checked: root.checked;
enabled: root.enabled;
control-label: root.title;
toggled => { root.toggled(self.checked); }
}
}
}
// A small app-level wrapper keeps select geometry consistent while retaining
// the platform widget's keyboard navigation, popup behavior, and accessibility.
export component SettingsSelect inherits ComboBox {
in property <[string]> choices;
in-out property <int> selected-index: 0;
callback selection-changed(int, string);
model: root.choices;
current-index <=> root.selected-index;
height: 36px;
min-width: 180px;
selected(value) => {
root.selection-changed(root.selected-index, value);
}
}
export component SettingsSelectRow inherits Rectangle {
in property <bool> compact: false;
in property <length> compact-description-height: 34px;
in property <string> title;
in property <string> description;
in property <[string]> choices;
in-out property <int> selected-index: 0;
in property <color> title-color;
in property <color> description-color;
callback selected(int, string);
// Desktop: the texts overhang the box padding by design. Compact: two 8px
// insets, two 7px gaps and the 36px selector around the stacked texts.
// Both layouts stay instantiated so the row can measure its texts.
height: root.compact
? 66px + compact-title.height + compact-description.height
: 23px + desktop-title.height + desktop-description.height;
vertical-stretch: 0;
HorizontalBox {
visible: !root.compact;
spacing: 20px;
VerticalBox {
spacing: 2px;
desktop-title := Text {
height: max(18px, self.preferred-height);
text: root.title;
color: root.title-color;
font-size: TypeScale.body;
font-weight: 600;
overflow: elide;
}
desktop-description := Text {
height: max(17px, self.preferred-height);
text: root.description;
color: root.description-color;
font-size: TypeScale.caption;
overflow: elide;
}
}
Rectangle { horizontal-stretch: 1; }
SettingsSelect {
width: 210px;
choices: root.choices;
selected-index <=> root.selected-index;
selection-changed(index, value) => { root.selected(index, value); }
}
}
VerticalBox {
visible: root.compact;
padding: 8px;
spacing: 7px;
compact-title := Text {
height: max(20px, self.preferred-height);
text: root.title;
color: root.title-color;
font-size: TypeScale.body;
font-weight: 600;
wrap: word-wrap;
}
compact-description := Text {
height: max(root.compact-description-height, self.preferred-height);
text: root.description;
color: root.description-color;
font-size: TypeScale.caption;
wrap: word-wrap;
}
SettingsSelect {
horizontal-stretch: 1;
choices: root.choices;
selected-index <=> root.selected-index;
selection-changed(index, value) => { root.selected(index, value); }
}
}
}
export component SettingsLinkRow inherits Rectangle {
in property <string> text;
in property <string> description;
in property <image> icon;
in property <bool> colorize-icon: true;
in property <color> accent;
in property <color> body-text;
in property <color> muted-text;
in property <color> hover-background;
out property <bool> focus-visible: keyboard.focus-visible;
callback clicked;
height: 18px + link-title.height + link-description.height;
border-radius: 7px;
forward-focus: keyboard;
border-width: keyboard.focus-visible ? 2px : 0px;
border-color: root.accent;
background: link-hit.has-hover ? root.hover-background : transparent;
accessible-role: button;
accessible-label: root.text;
accessible-description: root.description;
accessible-action-default => { root.clicked(); }
Image {
x: 10px;
y: (parent.height - self.height) / 2;
width: 18px;
height: 18px;
source: root.icon;
colorize: root.colorize-icon ? root.accent : transparent;
image-fit: contain;
accessible-role: none;
}
VerticalBox {
x: 40px;
y: 7px;
width: parent.width - 80px;
height: parent.height - 14px;
spacing: 1px;
link-title := Text { height: max(18px, self.preferred-height); text: root.text; color: root.body-text; font-size: TypeScale.body; font-weight: 600; overflow: elide; }
link-description := Text { height: max(16px, self.preferred-height); text: root.description; color: root.muted-text; font-size: TypeScale.caption; overflow: elide; }
}
Image {
x: parent.width - 27px;
y: (parent.height - self.height) / 2;
width: 16px;
height: 16px;
source: @image-url("../icons/lucide/outline/external-link.svg");
colorize: root.accent;
image-fit: contain;
accessible-role: none;
}
keyboard := FocusVisibleScope {
accessible-role: none;
key-pressed(event) => {
if event.text == Key.Return || event.text == Key.Space { root.clicked(); return accept; }
reject
}
}
link-hit := TouchArea {
clicked => { keyboard.focus(); keyboard.pointer-activated(); root.clicked(); }
mouse-cursor: pointer;
}
}
// A card of shortcut rows under a small heading.
export component ShortcutSection inherits Rectangle {
in property <string> title;
in property <color> surface;
in property <color> card-border-color;
in property <color> heading;
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.card-border-color;
VerticalBox {
padding-top: 12px;
padding-right: 18px;
padding-bottom: 12px;
padding-left: 18px;
spacing: 0px;
Text {
height: max(20px, self.preferred-height);
text: root.title;
color: root.heading;
font-size: TypeScale.caption;
font-weight: 700;
vertical-alignment: center;
}
@children
}
}
// One shortcut with its keys; a second binding is shown as an alternative.
export component ShortcutRow inherits Rectangle {
in property <string> title;
in property <[keys]> keys;
in property <string> separator: @tr("or");
in property <color> title-color;
in property <color> separator-color;
height: max(40px, keys-row.preferred-height + 12px);
accessible-role: text;
accessible-label: @tr("{}, {}", root.title, root.keys.length > 1
? KeyboardKeys.text(root.keys[0]) + " " + root.separator + " " + KeyboardKeys.text(root.keys[1])
: KeyboardKeys.text(root.keys[0]));
HorizontalLayout {
spacing: 16px;
Text {
horizontal-stretch: 1;
text: root.title;
color: root.title-color;
font-size: TypeScale.body;
vertical-alignment: center;
overflow: elide;
accessible-role: none;
}
keys-row := HorizontalLayout {
spacing: 6px;
alignment: end;
cross-axis-alignment: center;
for binding[index] in root.keys : HorizontalLayout {
spacing: 6px;
cross-axis-alignment: center;
if index > 0 : Text {
text: root.separator;
color: root.separator-color;
font-size: TypeScale.caption;
vertical-alignment: center;
accessible-role: none;
}
KbdShortcut { keys: binding; }
}
}
}
}
export component FormField inherits VerticalBox {
in property <bool> enabled: true;
in property <string> label;
// Bind with `<=>`: typing replaces a one-way binding, after which the
// field would keep showing its old text when the form is reset or filled.
in-out property <string> value;
in property <string> placeholder;
in property <InputType> input-type: InputType.text;
in property <color> label-color;
callback edited(string);
forward-focus: field;
padding: 0px;
spacing: 5px;
// Long translated labels wrap on narrow screens instead of overflowing.
Text {
height: max(16px, self.preferred-height);
text: root.label;
color: root.label-color;
font-size: TypeScale.caption;
font-weight: 600;
vertical-alignment: center;
wrap: word-wrap;
}
field := LineEdit {
enabled: root.enabled;
height: 36px;
text <=> root.value;
placeholder-text: root.placeholder;
input-type: root.input-type;
edited(value) => { root.edited(value); }
}
}