feat(app-iced): Ink Cat / Silver Cat 换肤 —— 双色板默认 + 规格卡组件几何(M229)

产品出厂外观从 tone 派生的 `Scheme::baseline` 切到品牌色板:深色 = Ink(淡绿)、
浅色 = Silver(钢蓝),D38 取色链(背景图 / 壁纸)仍然优先,取色失败回落到色板。

主题三档接进设置页「通用 → 主题」:Ink / Silver / 跟随系统(落盘仍是
`appearance.theme_mode` 的 dark / light / auto,不新增配置键)。

组件几何按规格卡对齐(M228 的 `inkcat` 登记项逐条落地):
- 卡片圆角 12 → 16;实例元信息卡改用新的 ELEVATED CARD(无描边、SurfaceContainer
  底、内距 24、上标注 11/600)
- Snackbar 48/4 → 52/16(双行 72)
- Checkbox 20/rx2 → 18/rx4;Radio 20 → 18 Ø(内点仍 10)
- 按钮加 XS~XL 五档(产品仍只用 S=40),内距改走 ControlSize::pad_x
- 新增 FAB 四态(40/56/80/160×56)与图标按钮第四式 outlined
- 语言与主题两行 chips 统一成规格卡 chip(32 高 / rx8)

`theme.rs` 原本正好 400 行,先把单测纯搬移到 `theme/tests.rs` 再加色板字段;
`app/tests.rs`、`settings/general.rs` 同样按纯搬移拆分守 400 行硬线。

验证:cargo fmt --all --check、cargo check -p unlucky-app-iced --all-targets、
cargo test --workspace(除 clippy 前的一轮 flake,见报告)、cargo clippy --workspace
--all-targets -- -D warnings、全仓 400 行扫描。视觉矩阵 42 张在
DevDocs/artifacts/m229/(gitignored,不进提交)。
This commit is contained in:
AskaEth committed 2026-11-11 01:49:13 +08:00
1 parent a06b7ff4ec
commit db541c820e
44 files changed
+1439 -300

No files matched your search

+1 -2
View File
@@ -11,7 +11,6 @@ use iced::widget::{container, image, row, text};
use iced::{Alignment, Background, Border, ContentFit, Element, Length, Pixels};
use unlucky_design::color::Role;
use unlucky_design::component_map::Component;
use unlucky_design::elevation::MaterialLayer;
use unlucky_design::icons::IconKind;
use unlucky_design::shape::ShapeScale;
@@ -171,7 +170,7 @@ pub fn alert<'a>(
.padding(ui.metrics().card_padding)
.style(move |_theme| container::Style {
background: Some(Background::Color(ui.layer(MaterialLayer::Card))),
border: ui.outlined(Component::OutlinedCard.spec().shape),
border: ui.outlined(ui.card_shape()),
..container::Style::default()
})
.into()
@@ -59,7 +59,6 @@ use iced::{
};
use unlucky_design::color::Role;
use unlucky_design::component_map::Component;
use unlucky_design::elevation::MaterialLayer;
use unlucky_design::shape::ShapeScale;
use unlucky_design::spacing::Space;
@@ -100,7 +99,7 @@ pub fn section<'a>(ui: &'a Ui, state: &'a State, lang: Lang) -> Element<'a, Mess
/// 外观卡片:底色走 `ui.layer`(面板 α 的唯一来源),与其它页的卡片同源。
fn card<'a>(ui: &'a Ui, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
let spec = Component::OutlinedCard.spec();
let shape = ui.card_shape();
let background = ui.layer(MaterialLayer::Card);
container(content)
@@ -108,7 +107,7 @@ fn card<'a>(ui: &'a Ui, content: impl Into<Element<'a, Message>>) -> Element<'a,
.padding(ui.metrics().card_padding)
.style(move |_theme| container::Style {
background: Some(Background::Color(background)),
border: ui.outlined(spec.shape),
border: ui.outlined(shape),
..container::Style::default()
})
.into()
+7 -3
View File
@@ -32,15 +32,19 @@ pub const SCROLLBAR_WIDTH: f32 = 6.0;
/// 滑块离卡片内边的留白(2px,避免贴边)。
pub const SCROLLBAR_MARGIN: f32 = 2.0;
/// 描边卡片(占位:M75 的 `components` 会提供真正的 `OutlinedCard`)。
/// 描边卡片(长表单容器)。圆角取**规格卡**的 16(`Ui::card_shape`),不是组件表的 12。
///
/// M229 换肤:规格卡 `CARD` 小节把卡片画成 `rx=16`,而 `component_map` 的 `OutlinedCard` 是
/// `Medium(12)`(官方值)。M228 把这条差异登记为「换肤时取 `inkcat::metrics::CARD`」,这里就是那次取值;
/// 组件表的值不动(它还服务别的面)。描边仍走 `OutlineVariant` 1px。
pub fn card<'a>(ui: &'a Ui, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
let spec = Component::OutlinedCard.spec();
let shape = ui.card_shape();
container(content)
.width(Length::Fill)
.padding(ui.metrics().card_padding)
.style(move |_theme| container::Style {
background: Some(Background::Color(ui.layer(MaterialLayer::Card))),
border: ui.outlined(spec.shape),
border: ui.outlined(shape),
..container::Style::default()
})
.into()
+8 -6
View File
@@ -36,13 +36,15 @@ fn empty_state_follows_the_12b_reference() {
const { assert!(EMPTY_ICON_GLYPH < EMPTY_ICON_BLOCK) };
}
/// 卡片圆角取**规格卡**的 16(M229 换肤),组件表 `OutlinedCard` 的 12 只作为对照保留。
#[test]
fn card_uses_the_outlined_card_spec() {
let spec = Component::OutlinedCard.spec();
assert_eq!(spec.shape, ShapeScale::Medium);
assert_eq!(ui().radius_dp(spec.shape).top_left, 12.0);
assert_eq!(spec.text, TypeRole::TitleMedium);
assert_eq!(spec.support, Some(TypeRole::BodySmall));
fn card_uses_the_spec_corner_not_the_component_table() {
let ui = ui();
assert_eq!(ui.card_shape(), ShapeScale::Large);
assert_eq!(ui.radius_dp(ui.card_shape()).top_left, 16.0);
// 组件表的官方值仍是 12(表不动,差异登记在 `inkcat` 模块与 M229 报告里)。
assert_eq!(Component::OutlinedCard.spec().shape, ShapeScale::Medium);
assert_eq!(ui.radius_dp(ShapeScale::Medium).top_left, 12.0);
}
#[test]
+12 -19
View File
@@ -15,6 +15,7 @@ use unlucky_presenter::router::Route;
use crate::app::components::buttons::{self, Button};
use crate::app::components::divider;
use crate::app::components::interaction::Interaction;
use crate::app::components::surfaces::elevated;
use crate::theme::Ui;
use unlucky_design::icons::IconKind;
@@ -229,26 +230,18 @@ fn meta_grid<'a>(ui: &'a Ui, metrics: FrameMetrics, detail: &'a Detail) -> Eleme
for chunk in detail.meta.chunks(columns) {
let mut cells_row = row![].spacing(layout::META_GAP).width(Length::Fill);
for cell in chunk {
let card = frame::card(
// M229:元信息卡 = 规格卡的 **ELEVATED CARD**(无描边、`SurfaceContainer` 底、
// 16 圆角、内距 24;上标注 11/600 + 值 16/400)。换肤前这里是描边卡。
let card = elevated::card(
ui,
column![
frame::text_role(
ui,
TypeRole::LabelMedium,
false,
cell.label.as_str(),
ui.color(Role::OnSurfaceVariant),
),
frame::text_role(
ui,
TypeRole::BodyMedium,
false,
cell.value.as_str(),
ui.color(Role::OnSurface)
),
]
.spacing(ui.space(Space::X1))
.width(Length::Fill),
elevated::texts(
ui,
elevated::Texts {
overline: Some(cell.label.as_str()),
title: cell.value.as_str(),
support: None,
},
),
);
cells_row = cells_row.push(container(card).width(Length::Fill));
}
+1 -2
View File
@@ -18,7 +18,6 @@
use iced::widget::{container, scrollable};
use iced::{Background, Element, Length};
use unlucky_design::component_map::Component;
use unlucky_design::elevation::MaterialLayer;
use crate::theme::Ui;
@@ -33,7 +32,7 @@ pub fn card<'a>(
height: Length,
content: impl Into<Element<'a, Message>>,
) -> Element<'a, Message> {
let shape = Component::OutlinedCard.spec().shape;
let shape = ui.card_shape();
container(content)
.width(Length::Fill)
+6
View File
@@ -8,6 +8,7 @@
//! 同一条纪律);app 层只做一次 `.map`。
use unlucky_i18n::LangChoice;
use unlucky_presenter::prefs::ThemeMode;
use unlucky_presenter::router::Route;
use super::appearance;
@@ -29,6 +30,11 @@ pub enum Message {
Launch(String),
/// 停止某实例(app 层翻成 `AppCommand::StopInstance`)。
Stop(String),
/// 切主题档(M229:设置页「通用 → 主题」的 Ink / Silver / 跟随系统三档)。
///
/// 载荷是 `ThemeMode` 而不是色板枚举:Ink / Silver 就是 `dark` / `light` 两侧的**产品名**,
/// 第三档 `auto` 由系统深浅落到对应色板。落盘键仍是 `appearance.theme_mode`。
Theme(ThemeMode),
/// 外观项改动(设置页「外观」分组 → `App` 的状态与落盘)。
///
/// 载荷是**用户请求值**(未钳位):钳位与 CPU 档降级在 `appearance::alpha` 里做,
+40 -15
View File
@@ -29,9 +29,11 @@
//! 3. **文案全部走 catalog**([`Key`]),三档选项名跟着界面语言走(见 `catalog/general.rs`
//! 的头注释:为什么不写语言自称)。
//!
//! ## 其余三行仍只读
//! ## 主题行已接真控件(M229),另两行仍只读
//!
//! 主题 / 启动页 / 关闭按钮还没有后端(`pages::Action` 里是占位动作),所以它们是**只读行**;
//! 主题三档(Ink / Silver / 跟随系统)在 M229 接成真控件:点一下换色板 + 落盘
//! `appearance.theme_mode`(实现在 `general/theme.rs`,消息是 `pages::Message::Theme`)。
//! 启动页 / 关闭按钮还没有后端(`pages::Action` 里是占位动作),仍是**只读行**;
//! 但标签与取值已经进 catalog,中文界面下不会再冒出英文。
//!
//! 分组元数据只有一份(这里的 [`GROUP`]):`settings/mod.rs` 的 `GROUPS` 直接引用它。
@@ -41,7 +43,6 @@ use iced::{Alignment, Background, Border, Element, Length, Padding, Shadow, Them
use unlucky_design::color::{Role, StateLayer};
use unlucky_design::icons::IconKind;
use unlucky_design::shape::ShapeScale;
use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole;
use unlucky_i18n::catalog::Key;
@@ -52,16 +53,18 @@ use crate::pages::{frame, Language, Message};
use crate::theme::Ui;
use super::frame::{card, divider};
use super::Group;
use super::{Group, States};
mod theme;
/// General 分组(左列名固定英文:左列是**导航**,与导航轨同源,不进 catalog)。
pub(super) const GROUP: Group = Group {
name: "General",
icon: IconKind::Settings,
// 只给 harness(`pages_demo` 走 `appearance: None` 时)用的演示行:产品路径走 [`body`]。
// 语言那一行**不在**这里——它的值是要跟着语言变的,写死在静态表里就是用户看到的那条假值。
// 语言与主题那两行**不在**这里——它们的值跟着语言/档位变,写死在静态表里就是用户看到的假值
// (语言 M106 移出、主题 M229 移出)。
rows: &[
("Theme", "Follow system"),
("Startup page", "Instances"),
("Close button", "Exit to tray"),
],
@@ -69,7 +72,7 @@ pub(super) const GROUP: Group = Group {
};
/// 这一组的正文(**产品路径**)。
pub(super) fn body<'a>(ui: &'a Ui, language: Language) -> Element<'a, Message> {
pub(super) fn body<'a>(ui: &'a Ui, states: States<'a>, language: Language) -> Element<'a, Message> {
let catalog = language.effective.catalog();
let mut stacked = column![].spacing(0.0).width(Length::Fill);
stacked = stacked.push(language_row(ui, language));
@@ -78,14 +81,22 @@ pub(super) fn body<'a>(ui: &'a Ui, language: Language) -> Element<'a, Message> {
stacked = stacked.push(divider(ui));
stacked = stacked.push(font_row(ui, language));
// M188:日志文件夹(路径 + 打开 + 复制路径)。放在「主题/启动页」之前——用户投诉
// M229:主题行接真控件(Ink / Silver / 跟随系统)。档位住在设置组的自有状态里
// (`language_appearance::State::theme`),与语言行同一个排版:标题 + 三枚互斥 chip + 一句说明。
stacked = stacked.push(divider(ui));
stacked = stacked.push(theme::view(
ui,
&catalog,
states.groups.language_appearance.theme,
));
// M188:日志文件夹(路径 + 打开 + 复制路径)。放在「启动页/关闭按钮」之前——用户投诉
// 「找不到崩溃日志」要的答案,不该埋在只读行后面。
stacked = stacked.push(divider(ui));
stacked = stacked.push(super::logs_row::folder_row(ui, &catalog));
// 主题 / 启动页 / 关闭按钮:还没有后端,只读;文案已经进 catalog。
// 启动页 / 关闭按钮:还没有后端,只读;文案已经进 catalog。
for (label, value) in [
(Key::SettingsTheme, Key::SettingsThemeFollowSystem),
(Key::SettingsStartupPage, Key::SettingsStartupInstances),
(Key::SettingsCloseButton, Key::SettingsCloseToTray),
] {
@@ -290,14 +301,20 @@ fn setting_row<'a>(ui: &'a Ui, label: &'a str, value: &'a str) -> Element<'a, Me
/// 一个互斥按钮(选中态 `SecondaryContainer`,未选中透明 + `OnSurfaceVariant`)。
///
/// 与 `pages/appearance/section/mod.rs` 的 `choice` 同款,但文案来自 catalog(那份是英文基线)。
/// 与 `pages/appearance/section/mod.rs` 的 `choice` 同款几何,但文案来自 catalog(那份是英文基线)。
///
/// **几何(M229)**:取规格卡的 chip(高 32、圆角 8、内距 16、未选中 1px `OutlineVariant`),
/// 与同屏的主题三档 chips 完全同源——两行都是「并排的互斥筛选」,长得不一样会让读者以为是
/// 两种控件(M229 第一版就是这样:语言 40 高药丸、主题 32 高方角,出图一眼就不齐)。
fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, choice: LangChoice) -> Element<'a, Message> {
let height = ui.metrics().control_height;
let box_ = unlucky_design::inkcat::metrics::CHIP;
let height = box_.height;
let content_color = if selected {
ui.color(Role::OnSecondaryContainer)
} else {
ui.color(Role::OnSurfaceVariant)
};
let border_color = ui.color(Role::OutlineVariant);
button(
container(frame::text_role(
@@ -310,7 +327,10 @@ fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, choice: LangChoice) -> El
.align_y(Alignment::Center),
)
.height(height)
.padding(Padding::from([0.0, ui.space(Space::X3)]))
.padding(Padding::from([
0.0,
unlucky_design::inkcat::metrics::CHIP_PAD_X,
]))
.style(move |_theme: &IcedTheme, status| {
let background = if selected {
Some(Background::Color(ui.color(Role::SecondaryContainer)))
@@ -328,8 +348,13 @@ fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, choice: LangChoice) -> El
background,
text_color: content_color,
border: Border {
radius: ui.radius(ShapeScale::Full, height),
..Border::default()
color: border_color,
width: if selected {
0.0
} else {
crate::theme::STROKE_PX
},
radius: ui.radius(box_.shape, height),
},
shadow: Shadow::default(),
snap: true,
@@ -0,0 +1,161 @@
//! 设置页「General → 主题」行:Ink / Silver / 跟随系统三档(M229)。
//!
//! 从 `general.rs` 拆出来的理由只有一条:加这三档之后那个文件 474 行(400 行硬线)。
//! 拆法是「整段搬走 + 一个 `pub(super) fn row` 入口」,排版与 handler 一字未改。
//!
//! 三档的语义:**Ink = 深色主方案、Silver = 浅色主方案**,落盘取值仍是 `appearance.theme_mode`
//! 的 `dark` / `light`;第三档「跟随系统」= `auto`,由系统深浅落到对应色板
//! (`BrandBoard::for_flavor`)。所以这里**不新增配置键**,旧配置读进来语义也一致。
use iced::widget::{button, column, container, row};
use iced::{Alignment, Background, Border, Element, Length, Padding, Shadow, Theme as IcedTheme};
use unlucky_design::color::{Role, StateLayer};
use unlucky_design::inkcat::metrics as chipbox;
use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole;
use unlucky_i18n::catalog::{Catalog, Key};
use unlucky_presenter::prefs::ThemeMode;
use crate::pages::{frame, Message};
use crate::theme::Ui;
/// 三档全序(Ink / Silver / 跟随系统):`ThemeMode` 在 presenter,声明序是 `Auto/Light/Dark`,
/// 画出来的顺序按「深 → 浅 → 跟随」,所以这里另给一张表,不靠枚举声明序。
pub(super) const THEME_MODES: [ThemeMode; 3] = [ThemeMode::Dark, ThemeMode::Light, ThemeMode::Auto];
/// 主题行:标题 + 三档互斥 chip + 一句说明(排版与语言行同族)。
pub(super) fn view<'a>(ui: &'a Ui, catalog: &Catalog, mode: ThemeMode) -> Element<'a, Message> {
let head = row![
container(frame::text_role(
ui,
TypeRole::BodyMedium,
false,
catalog.text(Key::SettingsTheme),
ui.color(Role::OnSurface),
))
.width(Length::Fill)
.align_y(Alignment::Center),
frame::text_role(
ui,
TypeRole::LabelLarge,
true,
catalog.text(mode_label(mode)),
ui.color(Role::OnPrimaryFixedVariant),
),
]
.width(Length::Fill)
.align_y(Alignment::Center);
let chips = row(THEME_MODES
.iter()
.map(|choice| {
chip(
ui,
catalog.text(mode_label(*choice)),
*choice == mode,
*choice,
)
})
.collect::<Vec<_>>())
.spacing(ui.space(Space::X1))
.align_y(Alignment::Center);
column![
head,
chips,
frame::text_role(
ui,
TypeRole::BodySmall,
false,
catalog.text(Key::SettingsThemeHint),
ui.color(Role::OnSurfaceVariant),
),
]
.spacing(ui.space(Space::X1))
.width(Length::Fill)
.padding(Padding::from([ui.space(Space::X2), 0.0]))
.into()
}
/// 一档主题的选项名(Ink / Silver / 跟随系统)。
fn mode_label(mode: ThemeMode) -> Key {
match mode {
ThemeMode::Dark => Key::SettingsThemeInk,
ThemeMode::Light => Key::SettingsThemeSilver,
ThemeMode::Auto => Key::SettingsThemeFollowSystem,
}
}
/// 一枚主题档位按钮(选中态 `SecondaryContainer`,未选中透明 + 1px `OutlineVariant`)。
///
/// 几何取规格卡的 chip(高 32、圆角 8,`inkcat::metrics::CHIP`):主题三档是**可并排的筛选**,
/// 不是主行动按钮,用 chip 档比语言行那枚 40 高药丸更贴规格卡。
fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, mode: ThemeMode) -> Element<'a, Message> {
let box_ = chipbox::CHIP;
let height = box_.height;
let content_color = if selected {
ui.color(Role::OnSecondaryContainer)
} else {
ui.color(Role::OnSurfaceVariant)
};
let border_color = ui.color(Role::OutlineVariant);
let stroke = crate::theme::STROKE_PX;
button(
container(frame::text_role(
ui,
TypeRole::LabelLarge,
selected,
text,
content_color,
))
.align_y(Alignment::Center),
)
.height(height)
.padding(Padding::from([0.0, chipbox::CHIP_PAD_X]))
.style(move |_theme: &IcedTheme, status| {
let background = if selected {
Some(Background::Color(ui.color(Role::SecondaryContainer)))
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
Some(Background::Color(ui.state_over(
StateLayer::Hover,
Role::OnSurface,
Role::Surface,
)))
} else {
None
};
button::Style {
background,
text_color: content_color,
border: Border {
color: border_color,
width: if selected { 0.0 } else { stroke },
radius: ui.radius(box_.shape, height),
},
shadow: Shadow::default(),
snap: true,
}
})
.on_press(Message::Theme(mode))
.into()
}
#[cfg(test)]
mod tests {
use super::*;
/// 三档 = 两块板 + 一种跟随,顺序是深 → 浅 → 跟随(换序会让「Ink 在最左」这条观感失守)。
#[test]
fn the_three_modes_are_ink_silver_and_follow_system() {
assert_eq!(
THEME_MODES,
[ThemeMode::Dark, ThemeMode::Light, ThemeMode::Auto]
);
assert_eq!(mode_label(ThemeMode::Dark), Key::SettingsThemeInk);
assert_eq!(mode_label(ThemeMode::Light), Key::SettingsThemeSilver);
assert_eq!(mode_label(ThemeMode::Auto), Key::SettingsThemeFollowSystem);
}
}
@@ -21,10 +21,16 @@
/// 语言 + 外观线的状态(**只归本线**)。
///
/// 首版是空结构:这一条线还没有自己的运行期状态(外观那份状态是 `pages::appearance::State`,
/// 归 M93,走 `States::appearance` 那条既有通道,不在这里)。加字段只改本文件。
/// 外观那份运行期状态是 `pages::appearance::State`(M93),走 `States::appearance` 那条既有通道,
/// 不在这里。本结构只装**分组自己的**档位:M229 起主题三档(Ink / Silver / 跟随系统)住这儿,
/// 由 `app` 在启动与切换时同步(唯一写者见 `app/state/theme.rs::set_theme_mode`)。
///
/// 初值 `Auto`:harness / 演示路径没有配置可读,跟随系统正是出厂值。
#[derive(Debug, Default)]
pub struct State {}
pub struct State {
/// 当前主题档(Ink = `Dark` / Silver = `Light` / 跟随系统 = `Auto`)。
pub theme: unlucky_presenter::prefs::ThemeMode,
}
/// 语言 + 外观线的消息(**只归本线**)。
///
+1 -1
View File
@@ -358,7 +358,7 @@ fn section_columns<'a>(
// 档位与生效语言不在 [`States`] 里,只能从 `pages::Language` 进来;它与 `appearance` 在不在场
// 无关(harness 那条路径也画得出真控件)。
let card = if index == GENERAL_GROUP {
general::body(ui, language)
general::body(ui, states, language)
} else if index == UPDATE_GROUP {
// M159:更新组读 presenter 快照(`view` 的 `update` 形参),与 General 同一条按下标分发。
update::body(ui, update, language.effective)
@@ -17,20 +17,19 @@ use unlucky_design::icons::IconKind;
use unlucky_i18n::LangChoice;
use unlucky_i18n::{Key, Lang};
/// 这一组的**静态演示行**:语言那一行已经换真控件,所以只剩三行。
/// 这一组的**静态演示行**:语言(M106)与主题(M229)都已经换成真控件,所以只剩两行。
///
/// 抓得住:①四条线并行合并时把别人的行粘到本文件;②语言行被「顺手」加回静态表——
/// 那等于把用户看到的假值又写回去(它的值必须跟着语言变,静态表做不到)。
/// 抓不住:真控件画出来长什么样(那是截图验收的事,见 `DevDocs/artifacts/m106/`)。
/// 抓得住:①四条线并行合并时把别人的行粘到本文件;②语言/主题被「顺手」加回静态表——
/// 那等于把用户看到的假值又写回去(它们的值必须跟着语言/档位变,静态表做不到)。
/// 抓不住:真控件画出来长什么样(那是截图验收的事,见 `DevDocs/artifacts/m229/`)。
#[test]
fn the_general_group_keeps_theme_startup_and_close_as_static_rows() {
fn the_general_group_keeps_only_the_two_rows_without_a_backend() {
let rows: Vec<(&str, &str)> = general::GROUP.rows.to_vec();
assert_eq!(
rows,
vec![
("Theme", "Follow system"),
("Startup page", "Instances"),
("Close button", "Exit to tray"),
("Close button", "Exit to tray")
]
);
}