Files
crossdesk/apps/desktop/src/gui/ui/main_window.slint
T

2729 lines
144 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { UiRadius } from "ui_radius.slint";
import { WorkbenchStyle, WorkbenchButton, WorkbenchCard, WorkbenchNotice } from "workbench_controls.slint";
import { GalleryStrings } from "device_strings.slint";
import { DeviceCardStyle } from "device_card_actions.slint";
import { GalleryMenuButton } from "gallery_menu_button.slint";
import { DeviceGroupDialog, DeviceGroupOption } from "device_group_dialog.slint";
import { GalleryView, GalleryTile, GalleryRecentDevice, GalleryActionDevice } from "gallery_view.slint";
import { GalleryNetworkDialog, GalleryNetworkDevice } from "gallery_network_dialog.slint";
import { SettingsDialog, SettingsButton, SettingsSelect, SettingsGroup, SettingsRow, SettingsToggleRow } from "settings_controls.slint";
import { GallerySettings } from "gallery_settings.slint";
import { GroupFilter } from "group_filter.slint";
import { ScrollView } from "std-widgets.slint";
import { RoundedLineEdit } from "rounded_line_edit.slint";
import { AnnouncementItem, MessagePanel, MessageStrings } from "message_panel.slint";
import { FontAwesomeIcons, IconButton, CompactComboBox, ImGuiCheckBox, CompactButton, PrimaryButton, SecondaryButton, ToggleSwitch, SectionTitle, DialogSurface, ImGuiLineStyle, ImGuiFontStyle } from "common.slint";
import "../assets/fonts/message_spacing.ttf";
export struct RecentConnection {
remote-id: string,
display-name: string,
host-name: string,
group-names: string,
online: bool,
connected: bool,
thumbnail: image,
has-thumbnail: bool,
platform: string,
}
export struct ReleaseNoteBlock {
content: styled-text,
section-gap: bool,
}
export enum UpdateCheckStatus { idle, checking, up-to-date, available, failed }
export enum AutostartStatus { checking, enabled, disabled, unsupported, failed }
enum CopyFeedback { idle, success, failed }
enum SponsorPlatform { afdian, kofi, github }
export global UiStrings {
in-out property <string> local-desktop: "Local Desktop";
in-out property <string> remote-desktop: "Remote Desktop";
in-out property <string> recent-connections: "Recent Connections";
in-out property <string> local-id: "Local ID";
in-out property <string> device-name: "Device Name";
in-out property <string> remote-id: "Remote ID";
in-out property <string> online: "Online";
in-out property <string> offline: "Offline";
in-out property <string> password: "Password";
in-out property <string> refresh-password: "Refresh random password";
in-out property <string> copied: "Local ID copied to clipboard";
in-out property <string> connect: "Connect";
in-out property <string> settings: "Settings";
in-out property <string> about: "About";
in-out property <string> sponsorship-title: "Sponsorship";
in-out property <string> sponsor-via-afdian: "AFDIAN";
in-out property <string> ok: "OK";
in-out property <string> cancel: "Cancel";
in-out property <string> save: "Save";
in-out property <string> new-password: "New password (6 characters)";
in-out property <string> invalid-password: "Password must contain exactly 6 characters";
in-out property <string> edit-alias: "Connection alias";
in-out property <string> delete-connection: "Delete connection";
in-out property <string> confirm-delete: "Delete this recent connection?";
in-out property <string> language: "Language";
in-out property <string> capture-method: "Capture method";
in-out property <string> capture-method-auto: "Auto";
in-out property <string> codec: "Video codec";
in-out property <string> hardware-codec: "Hardware video codec";
in-out property <string> turn-relay: "TURN relay";
in-out property <string> force-relay: "Force relay";
in-out property <string> self-hosted: "Self-hosted server";
in-out property <string> autostart: "Start with system";
in-out property <string> daemon: "Run in background";
in-out property <string> save-remote-previews: "Save remote screen previews:";
in-out property <string> unknown-platform: "Unknown platform";
in-out property <string> privacy-on-connect: "Privacy screen on connect";
in-out property <string> file-save-path: "File save path";
in-out property <string> default-desktop: "Desktop";
in-out property <string> server-host: "Server host";
in-out property <string> server-port: "Signal port";
in-out property <string> version: "Version";
in-out property <string> copyright: "© 2026 by JUNKUN DI. All right reserved.";
in-out property <string> license: "Licensed under GNU GPL v3.";
in-out property <string> signal-connected: "Signal server connected";
in-out property <string> signal-disconnected: "Signal server disconnected";
in-out property <string> tls-error: "Signal server TLS certificate error";
in-out property <string> signal-reconnect: "Reconnect";
in-out property <string> signal-reconnecting: "Reconnecting...";
in-out property <string> update-available: "A new version is available";
in-out property <string> new-version: "New version";
in-out property <string> check-for-updates: "Check for updates";
in-out property <string> checking-for-updates: "Checking for updates…";
in-out property <string> up-to-date: "You’re up to date";
in-out property <string> update-check-failed: "Check failed. Please try again later.";
in-out property <string> release-notes: "Release notes";
in-out property <string> view-release-notes: "Release notes";
in-out property <string> later: "Later";
in-out property <string> download: "Download";
in-out property <string> input-password: "Input password";
in-out property <string> reinput-password: "Incorrect password, please try again";
in-out property <string> remember-password: "Remember password";
in-out property <string> validate-password: "Validating password";
in-out property <string> request-permissions: "Request permissions";
in-out property <string> permission-required: "The application requires the following permissions:";
in-out property <string> screen-recording-permission: "Screen recording permission";
in-out property <string> accessibility-permission: "Accessibility permission";
in-out property <string> wayland-notice-title: "Wayland Session Notice";
in-out property <string> wayland-notice-subtitle;
in-out property <string> wayland-permission-title;
in-out property <string> wayland-notice-message;
in-out property <string> wayland-recommendation-title;
in-out property <string> wayland-recommendation-message;
in-out property <string> wayland-notice-acknowledge: "Got it";
in-out property <string> service-setup-title: "Install CrossDesk Service";
in-out property <string> service-setup-message: "Administrator permission is required to control this device while it is locked.";
in-out property <string> service-settings-label: "Lock Screen Service:";
in-out property <string> install-service: "Install";
in-out property <string> service-installed: "Installed";
in-out property <string> do-not-remind: "Do not remind again";
in-out property <string> notification: "Notification";
in-out property <string> service-suppressed-message: "You will not be reminded again. You can install the service later in Settings.";
in-out property <string> codec-h264: "H.264";
in-out property <string> codec-av1: "AV1";
in-out property <string> self-hosted-settings: "Self-hosted server settings";
in-out property <string> access-website: "Website: ";
in-out property <string> release-date-label: "Release Date: ";
}
// Keep sponsor labels, order, and sizing consistent across both dialogs.
component SponsorshipSection inherits VerticalLayout {
callback open-afdian;
callback open-kofi;
callback open-github;
vertical-stretch: 0;
spacing: 10px;
Text {
text: UiStrings.sponsorship-title;
color: WorkbenchStyle.text; font-size: 12px; font-weight: 600;
wrap: word-wrap;
}
HorizontalLayout {
spacing: 8px;
for platform in (GalleryStrings.language == 0
? [SponsorPlatform.afdian, SponsorPlatform.kofi, SponsorPlatform.github]
: [SponsorPlatform.kofi, SponsorPlatform.github, SponsorPlatform.afdian]): Rectangle {
// Size to the label, icon, and padding, then share extra row space.
min-width: sponsor-label-metrics.preferred-width + 56px;
preferred-width: self.min-width;
horizontal-stretch: 1;
height: 32px;
SettingsButton {
width: parent.width; height: parent.height;
icon: platform == SponsorPlatform.afdian ? @image-url("icons/sponsors/afdian.png")
: platform == SponsorPlatform.kofi ? @image-url("icons/sponsors/kofi.png")
: @image-url("icons/sponsors/github.svg");
text: platform == SponsorPlatform.afdian ? UiStrings.sponsor-via-afdian
: platform == SponsorPlatform.kofi ? "Ko-fi" : "GitHub Sponsors";
sponsor-label-metrics := Text {
visible: false; text: parent.text; font-size: 12px;
}
clicked => {
if platform == SponsorPlatform.afdian { root.open-afdian(); }
else if platform == SponsorPlatform.kofi { root.open-kofi(); }
else { root.open-github(); }
}
}
}
}
}
// Prompt copy in modal dialogs sits slightly low in the legacy ImGui layout.
// Offset only the rendered text so input fields and action buttons keep their
// established geometry.
component DialogPromptText inherits Rectangle {
in property <string> prompt;
in property <color> text-color: #30343b;
in property <length> text-size: ImGuiFontStyle.base;
in property <int> text-weight: 400;
// Mirror Text's intrinsic layout constraints exactly. This makes the
// wrapper invisible to VerticalLayout: siblings retain the same geometry
// they had when the Text item was a direct child.
min-height: prompt-label.min-height;
preferred-height: prompt-label.preferred-height;
max-height: prompt-label.max-height;
vertical-stretch: prompt-label.vertical-stretch;
prompt-label := Text {
y: 10px;
width: parent.width;
text: root.prompt;
color: root.text-color;
font-size: root.text-size;
font-weight: root.text-weight;
wrap: word-wrap;
horizontal-alignment: center;
}
}
// Use the same compact, plain-text field for connection and password changes.
component PasswordInput inherits Rectangle {
in-out property <string> text;
in property <int> max-length: 0;
in property <length> text-size: ImGuiFontStyle.base;
private property <string> previous-text;
callback accepted;
width: 70px;
height: 23px;
background: white;
border-width: 1px;
border-color: ImGuiLineStyle.border;
border-radius: UiRadius.control;
forward-focus: editor;
init => { root.previous-text = root.text; }
editor := TextInput {
x: 5px; y: 1px; width: parent.width - 10px; height: parent.height - 2px;
text <=> root.text;
input-type: InputType.text;
single-line: true;
color: #202124;
font-size: root.text-size;
vertical-alignment: center;
selection-foreground-color: white;
selection-background-color: #2463c7;
edited => {
// Validate the resulting text so typing, paste, and replacement
// all obey the limit while deletion and selection still work.
if (root.max-length > 0 && self.text.character-count > root.max-length) {
self.text = root.previous-text;
} else {
root.previous-text = self.text;
}
}
accepted => { root.accepted(); }
}
Timer {
interval: 1ms;
running: true;
triggered => {
self.running = false;
editor.focus();
// Place the caret after any restored password, including UTF-8.
editor.select-all();
editor.clear-selection();
}
}
}
export component MainWindow inherits Window {
title: root.gallery-mode ? "CrossDesk " + GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub") : "CrossDesk";
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
preferred-width: root.minimum-window-width;
preferred-height: root.minimum-window-height;
min-width: root.minimum-window-width;
max-width: root.gallery-mode ? 16384px : root.minimum-window-width;
min-height: root.minimum-window-height;
max-height: root.gallery-mode ? 16384px : root.minimum-window-height;
no-frame: root.custom-titlebar;
background: root.wayland-titlebar ? transparent : white;
default-font-size: ImGuiFontStyle.base;
// Fixed role for this window instance; opening DeskHub creates another window.
in property <bool> gallery-mode: false;
in-out property <bool> gallery-simplified: false;
changed gallery-simplified => {
root.reset-gallery-transient();
root.gallery-action("view-mode", "");
focus-sink.focus();
}
in-out property <bool> gallery-network-open: false;
in property <int> gallery-resolution-index: 0;
in property <int> gallery-frame-rate-index: 0;
in property <int> gallery-bandwidth-index: 3;
in-out property <int> gallery-resolution-draft: 0;
in-out property <int> gallery-frame-rate-draft: 0;
in-out property <int> gallery-bandwidth-draft: 3;
in property <string> gallery-settings-error;
in property <[GalleryNetworkDevice]> gallery-network-devices;
changed gallery-network-open => {
if root.gallery-network-open { root.menu-open = false; root.gallery-action("network-open", ""); }
}
in property <bool> gallery-fullscreen: false;
in property <bool> gallery-transitioning: false;
changed gallery-transitioning => {
if root.gallery-transitioning {
// Preserve the preview's keyboard focus when only changing size.
if !root.gallery-controlling { focus-sink.focus(); }
root.menu-open = false;
root.recent-tooltip-visible = false;
}
}
in-out property <string> connection-input-error;
in property <bool> gallery-controlling: false;
in property <int> gallery-remote-cursor-shape: 0;
in property <bool> gallery-group-controlling: false;
out property <float> gallery-video-width: gallery.video-width;
out property <float> gallery-video-height: gallery.video-height;
out property <float> gallery-preview-width: gallery.preview-width;
out property <float> gallery-preview-height: gallery.preview-height;
callback gallery-pointer(PointerEventButton, PointerEventKind, length, length);
callback gallery-scroll(length, length, length, length);
callback gallery-key(string, bool, bool, bool, bool, bool);
in property <[GalleryTile]> gallery-tiles;
in property <[string]> gallery-groups;
in-out property <int> gallery-group-index: 0;
out property <int> gallery-columns: gallery.columns;
out property <int> gallery-rows: gallery.rows;
in-out property <float> gallery-tile-width: 216;
in-out property <float> gallery-focus-rail-height: 70.875;
out property <bool> gallery-resizing: gallery.resizing || gallery.focus-resizing;
in property <int> gallery-page: 0;
in property <int> gallery-pages: 1;
in property <int> gallery-count: 0;
in property <int> gallery-connected: 0;
in property <int> gallery-selected: 0;
in property <bool> gallery-selected-audio-available: false;
in property <bool> gallery-selected-audio-muted: false;
in property <int> gallery-hidden-selected: 0;
in property <string> gallery-notice;
in property <[GalleryActionDevice]> gallery-selected-devices;
in property <GalleryTile> gallery-focused;
in-out property <bool> gallery-batch: false;
in-out property <bool> gallery-adding: false;
in-out property <bool> gallery-importing: false;
in property <[GalleryRecentDevice]> gallery-recent-devices;
in property <int> gallery-recent-selected: 0;
in property <int> gallery-recent-available: 0;
in property <string> gallery-import-group;
callback open-gallery();
callback toggle-gallery-fullscreen();
callback gallery-action(string, string);
callback gallery-swap(string, string) -> bool;
callback gallery-add-device(string, string, string, bool) -> bool;
callback gallery-import-recent(string) -> bool;
in-out property <bool> device-group-open: false;
private property <string> device-group-id;
private property <string> device-group-name;
in property <[DeviceGroupOption]> device-group-options;
in property <string> device-group-error;
in-out property <string> device-group-new-name;
callback device-group-action(string, string);
callback device-group-save(string) -> bool;
function open-device-groups(id: string, name: string) {
root.gallery-action("release", "");
focus-sink.focus();
root.recent-tooltip-visible = false;
root.device-group-id = id;
root.device-group-name = name;
root.device-group-new-name = "";
root.device-group-action("open", id);
root.device-group-open = true;
}
public function reset-gallery-transient() { gallery.reset-transient(); root.menu-open = false; root.gallery-network-open = false; }
in property <bool> custom-titlebar: false;
in property <bool> wayland-titlebar: false;
in property <bool> window-active: true;
private property <length> titlebar-height:
root.custom-titlebar && !root.gallery-fullscreen ? (root.wayland-titlebar ? 32px : 31px) : 0px;
out property <length> minimum-window-width: 640px;
out property <length> minimum-window-height:
(root.gallery-mode ? 448px : 472px) + root.titlebar-height;
private property <length> wayland-corner-radius: 12px;
in property <string> local-id: "";
in property <string> local-password: "";
in-out property <string> remote-id-input: "";
in property <bool> password-visible: true;
in property <bool> password-change-pending: false;
in property <bool> signal-connected: false;
in property <bool> signal-reconnecting: false;
in property <bool> signal-tls-error: false;
in property <[RecentConnection]> recent-connections;
in property <[string]> recent-groups: [GalleryStrings.tr("全部设备", "All devices", "Все устройства"), GalleryStrings.tr("未分组", "Ungrouped", "Без группы")];
in-out property <int> recent-group-index: 0;
callback recent-group-selected(int);
changed recent-group-index => {
root.recent-tooltip-visible = false;
recent-scroll.viewport-x = 0px;
}
in property <[AnnouncementItem]> announcements;
in property <int> announcement-unread;
private property <string> announcement-unread-label:
root.announcement-unread > 99 ? "99+" : "" + root.announcement-unread;
in property <int> announcement-total;
in property <bool> announcement-loading;
in property <bool> announcement-failed;
in property <bool> announcement-read-save-failed;
in property <bool> announcement-delete-failed;
in property <bool> announcement-loaded;
in-out property <bool> messages-open: false;
in property <string> announcement-selected-id;
in property <string> announcement-selected-revision;
in property <string> announcement-selected-title;
in property <styled-text> announcement-selected-body;
in property <string> announcement-selected-date;
in property <bool> announcement-selected-read;
callback open-announcement(string, string);
callback open-announcement-link(string);
callback delete-announcement(string, string);
callback refresh-announcements;
callback read-all-announcements;
callback load-more-announcements;
in property <bool> update-available: false;
in property <UpdateCheckStatus> update-check-status: idle;
private property <string> update-status-text:
root.update-check-status == UpdateCheckStatus.checking ? UiStrings.checking-for-updates :
root.update-check-status == UpdateCheckStatus.up-to-date ? UiStrings.up-to-date :
root.update-check-status == UpdateCheckStatus.failed ? UiStrings.update-check-failed : "";
in property <string> current-version: "";
in property <string> latest-version: "";
in property <string> release-name: "";
in property <[ReleaseNoteBlock]> release-note-blocks;
in property <string> release-date: "";
in-out property <string> offline-warning: "";
in property <bool> connection-dialog-open: false;
in property <string> connection-status-text: "";
in property <bool> connection-pending: false;
in property <bool> connection-password-required: false;
in property <bool> connection-validating: false;
in-out property <string> connection-password: "";
in-out property <bool> connection-remember-password: false;
in property <bool> permission-dialog-open: false;
in property <bool> screen-recording-granted: true;
in property <bool> accessibility-granted: true;
in-out property <bool> wayland-notice-open: false;
in property <bool> portable-service-settings-visible: false;
in property <bool> portable-service-dialog-open: false;
in property <bool> portable-service-suppressed-notice-open: false;
in property <bool> portable-service-installed: false;
in property <bool> portable-service-installing: false;
in property <bool> portable-service-succeeded: false;
in property <string> portable-service-status: "";
in-out property <bool> portable-service-do-not-remind: false;
in-out property <int> language-index: 0;
in property <bool> capture-method-visible: false;
in-out property <int> capture-method-index: 0;
in-out property <int> codec-index: 0;
in-out property <bool> hardware-codec-enabled: true;
in-out property <bool> turn-enabled: true;
in-out property <bool> force-relay-enabled: false;
changed turn-enabled => {
if !root.turn-enabled { root.force-relay-enabled = false; }
}
in-out property <bool> self-hosted-enabled: false;
in-out property <bool> autostart-enabled: false;
in property <bool> autostart-system-managed: false;
in property <AutostartStatus> autostart-status: AutostartStatus.checking;
out property <string> autostart-status-text:
root.autostart-status == AutostartStatus.checking ? GalleryStrings.tr("正在读取…", "Checking…", "Проверка…") :
root.autostart-status == AutostartStatus.enabled ? GalleryStrings.tr("已开启", "Enabled", "Включён") :
root.autostart-status == AutostartStatus.disabled ? GalleryStrings.tr("未开启", "Disabled", "Выключен") :
root.autostart-status == AutostartStatus.unsupported ? GalleryStrings.tr("设备不支持", "Unsupported", "Не поддерживается") :
GalleryStrings.tr("读取失败", "Read failed", "Ошибка чтения");
in-out property <bool> autostart-guide-open: false;
changed autostart-guide-open => { if root.autostart-guide-open { focus-sink.focus(); } }
in-out property <bool> daemon-enabled: false;
in-out property <bool> save-remote-previews: true;
// Show the preference on every platform; only adapted hosts can change it.
in property <bool> privacy-setting-available: false;
in-out property <bool> privacy-on-connect-enabled: true;
in-out property <string> file-save-path: "";
in-out property <string> server-host: "";
in-out property <string> server-port: "";
in-out property <string> self-host-settings-error: "";
in property <bool> settings-session-active: false;
// Keep the setting visible on every platform. Unsupported builds expose it
// as disabled so the fixed settings layout never turns into a blank row.
in property <bool> hardware-codec-available: true;
callback copy-local-id() -> bool;
callback toggle-password-visibility;
callback refresh-password;
callback reset-password(string) -> bool;
callback format-remote-id(string) -> string;
callback connect-requested(string);
callback recent-connect(string);
callback recent-disconnect(string);
callback recent-edit-alias(string, string);
callback recent-delete(string);
callback save-settings() -> bool;
callback cancel-settings;
callback refresh-autostart-status;
callback save-self-hosted-settings() -> bool;
callback cancel-self-hosted-settings;
callback browse-save-path;
callback open-download;
callback open-afdian-sponsor;
callback open-github-sponsor;
callback open-kofi-sponsor;
callback check-for-updates;
callback signal-reconnect-requested;
callback connection-cancel;
callback connection-acknowledge;
callback connection-submit-password(string, bool);
callback reset-remote-id;
callback request-screen-recording-permission;
callback request-accessibility-permission;
callback install-portable-service;
callback cancel-portable-service;
callback acknowledge-portable-service;
callback acknowledge-portable-service-suppressed;
callback main-title-drag(int, length, length);
callback minimize-main-window;
callback close-main-window;
private property <bool> menu-open: false;
in-out property <bool> settings-open: false;
in-out property <int> settings-tab: 0;
private property <length> settings-scroll-y: 0px;
in-out property <bool> self-host-settings-open: false;
changed self-host-settings-open => {
if !root.self-host-settings-open { root.self-host-settings-error = ""; }
}
in-out property <bool> about-open: false;
// Follow the first available update. Dismissing the dialog replaces this
// binding, so background checks do not reopen it during this launch.
in-out property <bool> update-open: root.update-available;
in-out property <bool> reset-password-open: false;
in-out property <bool> reset-password-invalid: false;
in-out property <string> new-password-input: "";
in-out property <bool> alias-open: false;
private property <string> alias-remote-id: "";
in-out property <string> alias-input: "";
in-out property <bool> delete-open: false;
private property <string> delete-remote-id: "";
private property <bool> delete-from-gallery: false;
private property <CopyFeedback> copy-feedback: idle;
private property <bool> compact-language: root.language-index == 0;
private property <bool> recent-tooltip-visible: false;
private property <string> recent-tooltip-name: "";
private property <string> recent-tooltip-host: "";
private property <string> recent-tooltip-id: "";
private property <string> recent-tooltip-groups: "";
private property <bool> recent-tooltip-online: false;
private property <length> recent-tooltip-pointer-x: 0px;
private property <length> recent-tooltip-pointer-y: 0px;
changed menu-open => {
if root.menu-open { root.copy-feedback = CopyFeedback.idle; root.gallery-action("release", ""); }
}
changed connection-dialog-open => { if root.connection-dialog-open { root.gallery-action("release", ""); } }
changed permission-dialog-open => { if root.permission-dialog-open { root.gallery-action("release", ""); } }
changed settings-open => {
if root.settings-open {
root.settings-tab = 0;
root.settings-scroll-y = 0px;
root.menu-open = false;
root.gallery-network-open = false;
root.gallery-resolution-draft = root.gallery-resolution-index;
root.gallery-frame-rate-draft = root.gallery-frame-rate-index;
root.gallery-bandwidth-draft = root.gallery-bandwidth-index;
root.gallery-action("settings-open", "");
if root.autostart-system-managed { root.refresh-autostart-status(); }
focus-sink.focus();
} else {
root.autostart-guide-open = false;
}
}
changed wayland-notice-open => {
if root.wayland-notice-open { focus-sink.focus(); }
}
changed messages-open => {
if root.messages-open { focus-sink.focus(); }
}
changed about-open => {
if root.about-open { focus-sink.focus(); }
}
reset-remote-id => {
root.remote-id-input = "";
remote-id-editor.focus();
}
window-surface := Rectangle {
width: root.width;
height: root.height;
background: root.wayland-titlebar ? #f6f5f4 : transparent;
border-radius:
root.wayland-titlebar ? root.wayland-corner-radius : 0px;
clip: root.wayland-titlebar;
if root.custom-titlebar && root.wayland-titlebar && !root.gallery-fullscreen: Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: root.titlebar-height;
background: #f6f5f4;
z: 100;
Text {
x: 80px;
width: parent.width - 160px;
height: parent.height;
text: root.title;
color: root.window-active ? #3d3d3d : #747474;
font-size: 12px;
font-weight: 600;
horizontal-alignment: center;
vertical-alignment: center;
overflow: elide;
}
wayland-title-drag := TouchArea {
width: parent.width - 80px;
property <int> phase: self.pressed ? 1 : 0;
changed phase => {
if self.pressed { root.copy-feedback = CopyFeedback.idle; }
root.main-title-drag(phase, self.mouse-x, self.mouse-y);
}
moved => {
if (self.pressed) {
root.main-title-drag(2, self.mouse-x, self.mouse-y);
}
}
}
wayland-minimize-button := Rectangle {
x: parent.width - 74px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: wayland-minimize-touch.pressed ? #c0bfbc
: wayland-minimize-touch.has-hover ? #deddda
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Minimize";
accessible-action-default => { wayland-minimize-touch.clicked(); }
Rectangle {
x: 8px;
y: 13px;
width: 8px;
height: 0.75px;
background: root.window-active
|| wayland-minimize-touch.has-hover
|| wayland-minimize-touch.pressed ? #454545 : #898989;
}
wayland-minimize-touch := TouchArea {
clicked => { root.minimize-main-window(); }
}
}
wayland-close-button := Rectangle {
x: parent.width - 34px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: wayland-close-touch.pressed ? #b2161d
: wayland-close-touch.has-hover ? #e01b24
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Close";
accessible-action-default => { wayland-close-touch.clicked(); }
Path {
width: parent.width;
height: parent.height;
viewbox-width: 24;
viewbox-height: 24;
commands: "M 14.75 14.75 L 8.25 8.25 M 14.75 8.25 L 8.25 14.75";
stroke: wayland-close-touch.has-hover
|| wayland-close-touch.pressed ? white
: root.window-active ? #454545 : #898989;
stroke-width: 1px;
}
wayland-close-touch := TouchArea {
clicked => { root.close-main-window(); }
}
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: #d5d3d0;
}
}
if root.custom-titlebar && !root.wayland-titlebar && !root.gallery-fullscreen: Rectangle {
x: 0px;
y: 0px;
width: root.width;
height: 31px;
background: #f9f9f9;
z: 100;
Image {
x: 8px;
y: 7px;
width: 16px;
height: 16px;
source: @image-url("../../../resources/linux/crossdesk_16x16.png");
image-fit: contain;
}
Text {
x: 30px;
width: parent.width - 122px;
height: parent.height;
text: root.title;
color: #202124;
font-size: 12px;
vertical-alignment: center;
overflow: elide;
}
title-drag := TouchArea {
x: 0px;
width: parent.width - 92px;
property <int> phase: self.pressed ? 1 : 0;
changed phase => {
if self.pressed { root.copy-feedback = CopyFeedback.idle; }
root.main-title-drag(phase, self.mouse-x, self.mouse-y);
}
moved => { if (self.pressed) { root.main-title-drag(2, self.mouse-x, self.mouse-y); } }
}
minimize-button := Rectangle {
x: parent.width - 92px;
width: 46px;
height: parent.height;
background: minimize-touch.pressed ? #c7c7c7
: minimize-touch.has-hover ? #e5e5e5
: transparent;
accessible-role: button;
accessible-label: "Minimize";
accessible-action-default => { minimize-touch.clicked(); }
Text {
text: FontAwesomeIcons.minus;
color: #202124;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 10px;
horizontal-alignment: center;
vertical-alignment: center;
accessible-role: none;
}
minimize-touch := TouchArea { clicked => { root.minimize-main-window(); } }
}
close-button := Rectangle {
x: parent.width - 46px;
width: 46px;
height: parent.height;
background: close-touch.pressed ? #b3251b
: close-touch.has-hover ? #c42b1c
: transparent;
accessible-role: button;
accessible-label: "Close";
accessible-action-default => { close-touch.clicked(); }
Text {
text: FontAwesomeIcons.xmark;
color: close-touch.has-hover || close-touch.pressed ? white : #202124;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 12px;
horizontal-alignment: center;
vertical-alignment: center;
accessible-role: none;
}
close-touch := TouchArea { clicked => { root.close-main-window(); } }
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: #d4d4d4;
}
}
content-layer := Rectangle {
x: 0px;
y: root.titlebar-height;
width: root.width;
height: root.height - self.y;
clip: true;
Rectangle {
width: parent.width;
height: parent.height;
background: root.gallery-mode ? white : WorkbenchStyle.canvas;
border-width: 0px;
border-radius: 0px;
focus-sink := FocusScope {
x: 0px; y: 0px; width: 0px; height: 0px;
key-pressed(event) => {
if root.autostart-guide-open {
if event.text == Key.Return || event.text == Key.Escape { root.autostart-guide-open = false; }
return accept;
}
if root.wayland-notice-open {
if event.text == Key.Return || event.text == Key.Escape {
root.wayland-notice-open = false;
}
return accept;
}
if root.about-open && !root.update-open {
if event.text == Key.Escape { root.about-open = false; }
return accept;
}
if root.messages-open {
if event.text == Key.Escape { root.messages-open = false; }
return accept;
}
if root.settings-open && !root.self-host-settings-open {
if event.text == Key.Escape { root.cancel-settings(); root.settings-open = false; }
return accept;
}
return reject;
}
}
TouchArea {
pointer-event(event) => {
if event.kind == PointerEventKind.down { root.copy-feedback = CopyFeedback.idle; }
}
clicked => { focus-sink.focus(); }
}
// Slint owns the client area below the native macOS title bar. Draw
// the boundary here because AppKit's titlebar separator is not shown
// reliably when winit creates a window without an NSToolbar.
Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: 1px;
background: ImGuiLineStyle.control-separator;
z: 7;
}
// A compact application toolbar keeps global actions outside device cards.
// Center the 28px buttons above the cards with 6px of space on each side.
open-gallery-button := WorkbenchButton {
visible: !root.gallery-mode;
x: 20px; y: 6px;
width: 80px; height: 28px;
text: GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub");
z: 8;
clicked => { root.open-gallery(); }
}
menu-button := WorkbenchButton {
visible: !root.gallery-mode;
x: parent.width - 56px; y: open-gallery-button.y;
width: 36px; height: 28px;
icon: FontAwesomeIcons.bars;
tooltip: GalleryStrings.tr("更多", "More", "Ещё");
z: 8;
clicked => { root.gallery-action("release", ""); root.menu-open = !root.menu-open; }
}
// Unread messages take precedence so the menu has only one indicator.
if !root.gallery-mode && root.update-available && root.announcement-unread <= 0: Rectangle {
x: menu-button.x + menu-button.width - self.width + 4px;
y: menu-button.y - 1px;
width: 16px;
height: 16px;
border-radius: 8px;
border-width: 1px;
border-color: white;
background: WorkbenchStyle.accent;
opacity: root.menu-open || mod(animation-tick(), 2s) < 1s ? 1 : 0;
z: 9;
accessible-role: button;
accessible-label: UiStrings.update-available;
accessible-action-default => { root.menu-open = !root.menu-open; }
// Center the visible mark independently of font baseline metrics.
Rectangle {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: 2px;
height: 10px;
accessible-role: none;
Rectangle {
y: 0px;
width: parent.width;
height: 6px;
border-radius: 1px;
background: white;
}
Rectangle {
y: 8px;
width: parent.width;
height: 2px;
border-radius: 1px;
background: white;
}
}
TouchArea {
mouse-cursor: pointer;
clicked => { root.gallery-action("release", ""); root.menu-open = !root.menu-open; }
}
}
if !root.gallery-mode && root.announcement-unread > 0: Rectangle {
x: menu-button.x + menu-button.width - self.width + 4px;
y: menu-button.y - 1px;
width: max(16px, unread-badge-label.preferred-width + 8px);
height: 16px;
border-radius: 8px;
border-width: 1px;
border-color: white;
background: #d92d20;
z: 9;
accessible-role: button;
accessible-label: MessageStrings.messages + " · " + MessageStrings.unread + " " + root.announcement-unread;
accessible-action-default => { root.menu-open = !root.menu-open; }
unread-badge-label := Text {
text: root.announcement-unread-label;
color: white;
font-size: 10px;
font-weight: 600;
horizontal-alignment: center;
vertical-alignment: center;
accessible-role: none;
}
TouchArea {
mouse-cursor: pointer;
clicked => { root.gallery-action("release", ""); root.menu-open = !root.menu-open; }
}
}
Rectangle {
y: 0px; height: parent.height - 28px; visible: !root.gallery-mode;
// Two balanced cards keep identity and connection actions distinct.
Rectangle {
x: 20px; y: 40px; width: parent.width - 40px; height: 208px;
// Button tooltips extend below these cards into the recent section.
z: 1;
local-card := WorkbenchCard {
x: 0px; y: 0px;
width: (parent.width - 16px) / 2; height: parent.height;
Text { x: 16px; y: 14px; width: parent.width - 32px; height: 24px; overflow: elide; text: UiStrings.local-desktop; font-size: 16px; font-weight: 600; color: WorkbenchStyle.text; }
Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: GalleryStrings.tr("将 ID 和密码分享给连接方", "Share your ID and password to get connected", "Передайте ID и пароль для подключения"); font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; }
Text { x: 16px; y: 68px; height: 18px; text: UiStrings.local-id; font-size: 12px; color: WorkbenchStyle.muted; }
Rectangle {
x: 16px; y: 90px; width: parent.width - 32px; height: 40px;
// Raise the whole field: a child's z cannot escape its
// parent's stacking order above the following password row.
z: 1;
background: WorkbenchStyle.field-surface; border-radius: UiRadius.control;
border-width: 1px; border-color: WorkbenchStyle.border;
Text { x: 12px; width: parent.width - 64px; height: parent.height; text: root.local-id; font-size: 24px; font-weight: 600; letter-spacing: 1px; color: WorkbenchStyle.text; vertical-alignment: center; }
Rectangle { x: parent.width - 48px; y: 10px; width: 1px; height: parent.height - 20px; background: WorkbenchStyle.border; }
copy-button := WorkbenchButton {
x: parent.width - 42px; y: 2px; width: 36px; quiet: true;
icon: root.copy-feedback == CopyFeedback.success ? FontAwesomeIcons.check : FontAwesomeIcons.copy;
icon-color: root.copy-feedback == CopyFeedback.success ? WorkbenchStyle.success : WorkbenchStyle.muted;
tooltip: GalleryStrings.tr("复制本机 ID", "Copy local ID", "Копировать ID");
tooltip-suppressed: root.copy-feedback != CopyFeedback.idle;
// Let other controls receive the original click; moving
// focus away from Copy dismisses its transient feedback.
changed has-focus => {
if !self.has-focus { root.copy-feedback = CopyFeedback.idle; }
}
clicked => {
root.copy-feedback = root.copy-local-id() ? CopyFeedback.success : CopyFeedback.failed;
copy-timer.restart();
}
}
}
Text { x: 16px; y: 138px; height: 18px; text: UiStrings.password; font-size: 12px; color: WorkbenchStyle.muted; }
Rectangle {
x: 16px; y: 158px; width: parent.width - 32px; height: 36px;
background: WorkbenchStyle.field-surface; border-radius: UiRadius.control;
border-width: 1px; border-color: WorkbenchStyle.border;
Text { x: 12px; width: parent.width - 132px; height: parent.height; text: root.password-visible ? root.local-password : "••••••"; font-size: 20px; font-weight: 500; letter-spacing: 2px; color: WorkbenchStyle.text; vertical-alignment: center; }
Rectangle { x: parent.width - 118px; y: 8px; width: 1px; height: parent.height - 16px; background: WorkbenchStyle.border; }
WorkbenchButton {
x: parent.width - 112px; y: 0px; width: 36px; quiet: true;
icon: root.password-visible ? FontAwesomeIcons.eye : FontAwesomeIcons.eye-slash;
tooltip: root.password-visible ? GalleryStrings.tr("隐藏密码", "Hide password", "Скрыть пароль") : GalleryStrings.tr("显示密码", "Show password", "Показать пароль");
clicked => { root.toggle-password-visibility(); }
}
WorkbenchButton {
x: parent.width - 74px; y: 0px; width: 36px; quiet: true;
icon: FontAwesomeIcons.rotate; tooltip: UiStrings.refresh-password;
enabled: !root.password-change-pending;
clicked => { root.refresh-password(); }
}
WorkbenchButton {
x: parent.width - 36px; y: 0px; width: 36px; quiet: true;
icon: FontAwesomeIcons.pen;
tooltip: GalleryStrings.tr("修改密码", "Change password", "Изменить пароль");
enabled: !root.password-change-pending;
clicked => { root.reset-password-open = true; }
}
}
}
WorkbenchCard {
x: local-card.width + 16px; y: 0px; width: local-card.width; height: parent.height;
Text { x: 16px; y: 14px; width: parent.width - 32px; height: 24px; overflow: elide; text: UiStrings.remote-desktop; font-size: 16px; font-weight: 600; color: WorkbenchStyle.text; }
Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: GalleryStrings.tr("输入设备 ID,连接远程桌面", "Enter a device ID to start a remote session", "Введите ID для удалённого подключения"); font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; }
Text { x: 16px; y: 68px; height: 18px; text: UiStrings.remote-id; font-size: 12px; color: WorkbenchStyle.muted; }
Rectangle {
x: 16px; y: 90px; width: parent.width - 32px; height: 40px;
background: white; border-radius: UiRadius.control; border-width: 1px;
border-color: root.connection-input-error != "" ? #d65252 : remote-id-editor.has-focus ? WorkbenchStyle.accent : WorkbenchStyle.control-border;
animate border-color { duration: 120ms; }
if root.remote-id-input == "": Text {
x: 12px; width: parent.width - 24px; height: parent.height;
text: GalleryStrings.tr("请输入 9 位设备 ID", "Enter a 9-digit device ID", "Введите ID из 9 цифр");
font-size: 13px; color: #8290a4; vertical-alignment: center;
}
remote-id-editor := TextInput {
x: 12px; width: parent.width - 24px; height: parent.height;
text <=> root.remote-id-input; font-size: 24px; font-weight: 600; letter-spacing: 1px;
input-type: InputType.number; color: WorkbenchStyle.text;
accessible-label: UiStrings.remote-id;
vertical-alignment: center;
selection-foreground-color: white; selection-background-color: WorkbenchStyle.accent;
edited => {
root.connection-input-error = "";
// Enforce the digit limit before the next frame can
// render the text from typing, paste, or replacement.
let formatted = root.format-remote-id(self.text);
self.text = formatted;
// Formatted IDs are ASCII, so these are UTF-8 offsets.
self.set-selection-offsets(
formatted.character-count, formatted.character-count);
}
accepted => { if !root.connection-pending { root.connect-requested(root.remote-id-input); } }
}
}
Text {
x: 16px; y: 133px; width: parent.width - 32px; height: 24px;
text: root.connection-input-error;
font-size: 11px; color: #be3d42; wrap: word-wrap;
}
WorkbenchButton {
x: 16px; y: 158px; width: parent.width - 32px; height: 36px;
primary: true; icon: FontAwesomeIcons.arrow-right-long;
text: root.connection-pending ? GalleryStrings.tr("正在连接…", "Connecting…", "Подключение…") : UiStrings.connect;
enabled: !root.connection-pending;
clicked => { root.connect-requested(root.remote-id-input); }
}
}
}
// Recent connections: previews, presence badges, and separate primary/overflow actions.
recent-panel := Rectangle {
y: 260px;
height: parent.height - self.y;
Text { x: 20px; y: 0px; width: recent-group-filter.x - self.x - 12px; height: 32px; text: UiStrings.recent-connections; font-size: 15px; font-weight: 600; color: WorkbenchStyle.text; vertical-alignment: center; }
recent-group-filter := GroupFilter {
x: parent.width - self.width - 20px; y: 0px;
width: 155px; height: 32px;
normal-border-color: WorkbenchStyle.control-border;
hover-border-color: WorkbenchStyle.hover-border;
focus-border-color: WorkbenchStyle.accent;
model: root.recent-groups;
current-index <=> root.recent-group-index;
selected(index) => { root.recent-group-selected(index); }
}
recent-list := Rectangle {
x: 20px;
y: 36px;
width: parent.width - 40px;
height: 142px;
// A continuous inset surface groups the cards into one
// horizontal scrolling lane, including its scrollbar.
background: root.recent-connections.length > 0 ? #e6edf6 : transparent;
border-width: root.recent-connections.length > 0 ? 1px : 0px;
border-color: #c9d4e3;
border-radius: UiRadius.surface;
clip: true;
if root.recent-connections.length == 0: WorkbenchCard {
x: 0px; y: 6px; width: parent.width; height: 124px;
Rectangle {
x: 20px; y: 38px; width: 48px; height: 48px;
background: #f0f4fb; border-radius: UiRadius.surface;
Text { text: FontAwesomeIcons.display; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 20px; color: #8297b9; horizontal-alignment: center; vertical-alignment: center; }
}
Text {
x: 84px; y: 36px; width: parent.width - 104px; height: 24px;
text: root.recent-group-index == 0
? GalleryStrings.tr("暂无近期连接", "No recent connections", "Нет недавних подключений")
: GalleryStrings.tr("该分组暂无近期连接", "No recent connections in this group", "В этой группе нет недавних подключений");
color: WorkbenchStyle.text; font-size: 14px; font-weight: 600;
}
Text {
x: 84px; y: 68px; width: parent.width - 104px; height: 20px;
text: root.recent-group-index == 0
? GalleryStrings.tr("连接过的设备会显示在这里,方便下次快速访问。", "Devices you connect to will appear here for quick access.", "Устройства появятся здесь после первого подключения.")
: GalleryStrings.tr("切换设备分组,查看其他连接记录。", "Choose another group to see your other connections.", "Выберите другую группу для просмотра подключений.");
color: WorkbenchStyle.muted; font-size: 12px;
}
}
recent-scroll := ScrollView {
visible: root.recent-connections.length > 0;
x: 6px; y: 5px;
width: parent.width - 12px;
height: parent.height - 6px;
horizontal-stretch: 1;
changed viewport-x => {
root.recent-tooltip-visible = false;
root.copy-feedback = CopyFeedback.idle;
}
HorizontalLayout {
padding: 0px;
padding-bottom: 10px;
spacing: 12px;
alignment: start;
for connection[index] in root.recent-connections: connection-card := Rectangle {
width: (recent-scroll.width - 24px) / 3;
height: 126px;
background: transparent;
private property <bool> available: connection.online || connection.connected;
private property <length> content-width: self.width;
private property <length> scroll-offset: recent-scroll.viewport-x;
private property <string> device-id: connection.remote-id;
private property <int> group-index: root.recent-group-index;
changed scroll-offset => { card-menu.close-menu(); }
changed device-id => { card-menu.close-menu(); }
changed group-index => { card-menu.close-menu(); }
Rectangle {
width: connection-card.content-width;
height: 124px;
border-radius: UiRadius.surface;
background: white;
clip: true;
Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: 80px;
background: !connection-card.available ? DeviceCardStyle.offline-preview :
connection.platform == "windows" ? @linear-gradient(135deg, #1f61b3, #337ac4) :
connection.platform == "macos" ? @linear-gradient(135deg, #6e4aad, #8c63c4) :
connection.platform == "linux" ? @linear-gradient(135deg, #a14f14, #ba631f) :
connection.platform == "ios" ? @linear-gradient(135deg, #146e66, #1f8278) :
@linear-gradient(135deg, #596675, #737d8c);
if connection.has-thumbnail: Image {
width: parent.width;
height: parent.height;
source: connection.thumbnail;
image-fit: cover;
// Keep the saved preview recognizable under the offline gray.
opacity: connection-card.available ? 1 : 0.16;
}
if !connection.has-thumbnail: Text {
x: 8px;
width: parent.width - 16px;
height: parent.height;
text: connection.platform == "windows" ? "Windows" :
connection.platform == "macos" ? "macOS" :
connection.platform == "linux" ? "Linux" :
connection.platform == "ios" ? "iOS" : UiStrings.unknown-platform;
color: connection-card.available ? white : DeviceCardStyle.offline-placeholder;
font-size: 20px;
font-weight: 600;
horizontal-alignment: center;
vertical-alignment: center;
overflow: elide;
}
Rectangle {
x: 8px; y: 8px;
width: presence-label.preferred-width + 24px; height: 20px;
background: #fffffff0; border-radius: self.height / 2;
Rectangle {
x: 8px; y: (parent.height - self.height) / 2;
width: 5px; height: 5px; border-radius: self.height / 2;
background: connection-card.available ? WorkbenchStyle.success : WorkbenchStyle.muted;
}
presence-label := Text {
x: 17px; width: self.preferred-width; height: parent.height;
text: connection-card.available ? UiStrings.online : UiStrings.offline;
color: connection-card.available ? WorkbenchStyle.success : WorkbenchStyle.muted;
font-size: 10px; font-weight: 500; vertical-alignment: center;
}
}
}
Rectangle {
x: 0px;
y: 80px;
width: parent.width;
height: 44px;
background: white;
Rectangle { x: 0px; y: 0px; width: parent.width; height: 1px; background: WorkbenchStyle.separator; }
Text {
x: 12px; y: 5px; height: 19px;
width: parent.width - 56px;
text: connection.display-name;
color: WorkbenchStyle.text;
font-size: 13px; font-weight: 600;
overflow: elide; vertical-alignment: center;
}
Text {
x: 12px; y: 24px; height: 16px; width: parent.width - 56px;
text: connection.remote-id; font-size: 11px; color: WorkbenchStyle.muted;
overflow: elide;
}
}
// The software renderer only clips rectangular bounds.
// This outer border masks the four corners there too.
Rectangle {
x: -parent.border-radius;
y: -parent.border-radius;
width: parent.width + 2 * parent.border-radius;
height: parent.height + 2 * parent.border-radius;
border-width: parent.border-radius;
border-radius: 2 * parent.border-radius;
border-color: recent-list.background;
}
}
card-touch := TouchArea {
width: connection-card.content-width;
height: 124px;
mouse-cursor: pointer;
clicked => { root.copy-feedback = CopyFeedback.idle; root.recent-connect(connection.remote-id); }
image-touch := TouchArea {
x: 0px; y: 0px;
width: parent.width;
height: 80px;
private property <string> group-names: connection.group-names;
private property <bool> available: connection-card.available;
changed available => {
if self.has-hover && root.recent-tooltip-id == connection.remote-id {
root.recent-tooltip-online = self.available;
}
}
changed group-names => {
if self.has-hover && root.recent-tooltip-id == connection.remote-id {
root.recent-tooltip-groups = self.group-names;
}
}
clicked => { root.copy-feedback = CopyFeedback.idle; root.recent-connect(connection.remote-id); }
function show-details() {
if !self.has-hover { return; }
// Leave the menu trigger and its popup free of device details.
if card-menu.menu-open || (self.mouse-x >= card-menu.x - 4px
&& self.mouse-y <= card-menu.y + card-menu.height + 4px) {
root.recent-tooltip-visible = false;
return;
}
root.recent-tooltip-name = connection.display-name;
root.recent-tooltip-host = connection.host-name;
root.recent-tooltip-id = connection.remote-id;
root.recent-tooltip-groups = self.group-names;
root.recent-tooltip-online = self.available;
root.recent-tooltip-pointer-x = recent-panel.x + recent-list.x
+ recent-scroll.x + recent-scroll.viewport-x
+ connection-card.x + card-touch.x + self.x + self.mouse-x;
root.recent-tooltip-pointer-y = recent-panel.y + recent-list.y
+ recent-scroll.y + recent-scroll.viewport-y
+ connection-card.y + card-touch.y + self.y + self.mouse-y;
root.recent-tooltip-visible = true;
}
changed has-hover => {
if self.has-hover {
self.show-details();
} else if root.recent-tooltip-id == connection.remote-id {
root.recent-tooltip-visible = false;
}
}
// moved only reports pressed pointer movement;
// track both coordinates during ordinary hover.
changed mouse-x => { self.show-details(); }
changed mouse-y => { self.show-details(); }
}
card-menu := GalleryMenuButton {
x: parent.width - self.width - 8px; y: 8px;
width: 24px; height: 24px;
compact-trigger: true;
compact-menu: true;
enabled: !root.gallery-mode;
text: GalleryStrings.tr("更多操作", "More actions", "Другие действия");
menu-width: root.compact-language ? 116px : root.language-index == 2 ? 156px : 128px;
items: [
{label: GalleryStrings.tr("修改名称", "Rename", "Переименовать"), enabled: true},
{label: GalleryStrings.tr("修改分组", "Edit groups", "Изменить группы"), enabled: true},
{label: GalleryStrings.tr("删除记录", "Delete record", "Удалить запись"), enabled: true},
];
item-icons: [FontAwesomeIcons.pen, FontAwesomeIcons.tags, FontAwesomeIcons.trash-can];
destructive-index: 2;
opened => { root.recent-tooltip-visible = false; root.copy-feedback = CopyFeedback.idle; }
selected(action) => {
root.copy-feedback = CopyFeedback.idle;
if action == 0 {
root.alias-remote-id = connection.remote-id;
root.alias-input = connection.display-name;
root.alias-open = true;
} else if action == 1 {
root.open-device-groups(connection.remote-id, connection.display-name);
} else if action == 2 {
root.delete-remote-id = connection.remote-id;
root.delete-from-gallery = false;
root.delete-open = true;
}
}
}
WorkbenchButton {
x: parent.width - self.width - 8px; y: 88px;
width: 28px; height: 28px;
soft: true;
primary: !connection.connected;
icon: connection.connected ? FontAwesomeIcons.xmark : FontAwesomeIcons.arrow-right-long;
tooltip: connection.connected ? GalleryStrings.tr("断开连接", "Disconnect", "Отключить") : UiStrings.connect;
tooltip-above: true;
clicked => {
root.copy-feedback = CopyFeedback.idle;
root.recent-tooltip-visible = false;
if connection.connected { root.recent-disconnect(connection.remote-id); }
else { root.recent-connect(connection.remote-id); }
}
}
}
Rectangle {
width: parent.width; height: 124px;
border-width: 1px; border-radius: UiRadius.surface;
border-color: card-touch.has-hover || card-menu.menu-open ? WorkbenchStyle.hover-border : WorkbenchStyle.border;
animate border-color { duration: 120ms; }
}
}
}
}
}
}
}
gallery := GalleryView {
y: 0px; width: parent.width;
height: parent.height - self.y;
visible: root.gallery-mode;
simplified: root.gallery-simplified;
fullscreen: root.gallery-fullscreen;
toggle-simplified => { root.gallery-simplified = !root.gallery-simplified; }
toggle-fullscreen => { root.toggle-gallery-fullscreen(); }
window-active: root.window-active;
tile-width <=> root.gallery-tile-width;
focus-rail-height <=> root.gallery-focus-rail-height;
network-open => { root.gallery-network-open = true; }
controlling: root.gallery-controlling;
remote-cursor-shape: root.gallery-remote-cursor-shape;
group-controlling: root.gallery-group-controlling;
input-suspended: root.gallery-transitioning;
pointer-input(button, kind, x, y) => { root.gallery-pointer(button, kind, x, y); }
scroll-input(dx, dy, x, y) => { root.gallery-scroll(dx, dy, x, y); }
key-input(text, pressed, ctrl, alt, shift, meta) => { root.gallery-key(text, pressed, ctrl, alt, shift, meta); }
tiles: root.gallery-tiles; groups: root.gallery-groups;
group-index <=> root.gallery-group-index;
page: root.gallery-page; pages: root.gallery-pages; count: root.gallery-count;
connected-count: root.gallery-connected; selected-count: root.gallery-selected;
selected-audio-available: root.gallery-selected-audio-available;
selected-audio-muted: root.gallery-selected-audio-muted;
hidden-selected: root.gallery-hidden-selected; notice: root.gallery-notice;
selected-devices: root.gallery-selected-devices;
focused: root.gallery-focused; batch <=> root.gallery-batch; adding <=> root.gallery-adding;
importing <=> root.gallery-importing; recent-devices: root.gallery-recent-devices;
recent-selected: root.gallery-recent-selected;
recent-available: root.gallery-recent-available; import-group: root.gallery-import-group;
action(code, value) => { root.gallery-action(code, value); }
swap-devices(source, target) => { root.gallery-swap(source, target) }
add-device(id, password, group, remember) => { root.gallery-add-device(id, password, group, remember) }
import-recent(group) => { root.gallery-import-recent(group) }
edit-alias(id, name) => {
root.alias-remote-id = id; root.alias-input = name; root.alias-open = true;
}
edit-groups(id, name) => { root.open-device-groups(id, name); }
remove-device(id) => {
root.delete-remote-id = id; root.delete-from-gallery = true; root.delete-open = true;
}
}
// Server status and reconnect actions belong to the connection window.
if !root.gallery-mode: Rectangle {
y: parent.height - 28px;
height: 28px;
background: white;
Rectangle {
y: 0px;
height: 1px;
background: WorkbenchStyle.border;
}
Rectangle {
x: 20px; y: 11px; width: 6px; height: 6px; border-radius: 3px;
background: root.signal-tls-error || root.signal-reconnecting ? #d6932b : root.signal-connected ? WorkbenchStyle.success : #d65252;
}
Text {
x: 34px;
y: 0px;
height: parent.height;
text: root.signal-tls-error ? UiStrings.tls-error
: root.signal-connected ? UiStrings.signal-connected
: root.signal-reconnecting ? UiStrings.signal-reconnecting
: UiStrings.signal-disconnected;
color: WorkbenchStyle.muted;
font-size: 11px;
vertical-alignment: center;
}
if root.signal-connected && !root.signal-tls-error: Text {
x: parent.width - self.preferred-width - 20px; height: parent.height;
text: root.current-version;
font-size: 11px; color: WorkbenchStyle.muted; vertical-alignment: center;
}
if !root.signal-connected && !root.signal-tls-error: Rectangle {
x: parent.width - self.width - 20px;
y: 0px;
height: parent.height;
width: reconnect-label.preferred-width;
reconnect-label := Text {
text: UiStrings.signal-reconnect;
color: WorkbenchStyle.accent;
font-size: 11px;
horizontal-alignment: center;
vertical-alignment: center;
}
TouchArea {
mouse-cursor: pointer;
clicked => {
root.copy-feedback = CopyFeedback.idle;
root.signal-reconnect-requested();
}
}
}
}
}
// Shared device details sit outside the clipped recent list and wall rail,
// below the modal layer (z >= 39).
if (root.gallery-mode ? gallery.hovered-device.id != "" : root.recent-tooltip-visible)
&& !root.connection-dialog-open: device-tip := Rectangle {
private property <string> device-name: root.gallery-mode ? gallery.hovered-device.name : root.recent-tooltip-name;
private property <string> host-name: root.gallery-mode ? gallery.hovered-device.host-name : root.recent-tooltip-host;
private property <string> device-id: root.gallery-mode ? gallery.hovered-device.id : root.recent-tooltip-id;
private property <string> group-names: root.gallery-mode ? gallery.hovered-device.group-names : root.recent-tooltip-groups;
private property <bool> online: root.gallery-mode ? gallery.hovered-device.online : root.recent-tooltip-online;
private property <bool> presence-known: !root.gallery-mode || gallery.hovered-device.presence-known;
private property <bool> has-host-name: self.host-name != "" && self.host-name != self.device-name;
private property <color> status-color: self.online ? WorkbenchStyle.success : WorkbenchStyle.muted;
private property <length> label-width: max(tooltip-id-label.preferred-width, tooltip-group-label.preferred-width);
private property <length> pointer-x: root.gallery-mode ? gallery.x + gallery.device-tooltip-x : root.recent-tooltip-pointer-x;
private property <length> pointer-y: root.gallery-mode ? gallery.y + gallery.device-tooltip-y : root.recent-tooltip-pointer-y;
// Follow the pointer with a gap; change sides only when space runs out.
x: clamp(self.pointer-x + self.width + 12px <= parent.width - 8px
? self.pointer-x + 12px : self.pointer-x - self.width - 12px,
8px, max(8px, parent.width - self.width - 8px));
y: clamp(self.pointer-y - self.height - 12px >= 8px
? self.pointer-y - self.height - 12px : self.pointer-y + 16px,
8px, max(8px, parent.height - self.height - 8px));
// Fit the text instead of reserving empty space for short device names.
width: min(220px, parent.width - 16px, max(160px,
tooltip-name.preferred-width + 24px,
self.has-host-name ? tooltip-host.preferred-width + 24px : 0px,
tooltip-name-label.preferred-width + tooltip-status.width + 32px,
self.label-width + max(tooltip-id.preferred-width, tooltip-groups.preferred-width) + 32px));
height: tooltip-groups.y + tooltip-groups.height + 10px;
background: WorkbenchStyle.surface;
border-width: 1px;
border-color: #d5dfea;
border-radius: UiRadius.menu;
drop-shadow-color: #14294424;
drop-shadow-blur: 14px;
drop-shadow-offset-y: 4px;
z: 20;
tooltip-name-label := Text {
x: 12px; y: 10px;
width: tooltip-status.x - self.x - 8px; height: 18px;
text: UiStrings.device-name;
color: WorkbenchStyle.muted; font-size: 10px;
vertical-alignment: center; overflow: elide;
}
tooltip-status := Rectangle {
x: parent.width - self.width - 12px; y: 10px;
width: tooltip-status-text.preferred-width + 14px; height: 18px;
background: device-tip.online ? #e9f6ef : WorkbenchStyle.field-surface;
border-radius: self.height / 2;
tooltip-status-text := Text {
x: 7px; width: parent.width - 14px; height: parent.height;
text: device-tip.online ? UiStrings.online : device-tip.presence-known ? UiStrings.offline
: GalleryStrings.tr("未知", "Unknown", "Неизвестно");
color: device-tip.status-color; font-size: 10px; font-weight: 600;
vertical-alignment: center;
}
}
tooltip-name := Text {
x: 12px; y: 32px; width: parent.width - 24px;
height: self.preferred-height;
text: device-tip.device-name;
color: WorkbenchStyle.text; font-size: 12px; font-weight: 600;
wrap: word-wrap;
}
tooltip-host := Text {
x: 12px; y: tooltip-name.y + tooltip-name.height + 3px;
width: parent.width - 24px;
height: device-tip.has-host-name ? self.preferred-height : 0px;
visible: device-tip.has-host-name;
text: device-tip.host-name;
color: WorkbenchStyle.muted; font-size: 10px;
wrap: word-wrap;
}
tooltip-divider := Rectangle {
x: 12px; y: tooltip-name.y + tooltip-name.height
+ (device-tip.has-host-name ? tooltip-host.height + 3px : 0px) + 8px;
width: parent.width - 24px; height: 1px;
background: WorkbenchStyle.separator;
}
tooltip-id-label := Text {
x: 12px; y: tooltip-divider.y + 7px;
width: device-tip.label-width; height: 16px;
text: UiStrings.remote-id;
color: WorkbenchStyle.muted; font-size: 10px;
vertical-alignment: center;
}
tooltip-id := Text {
x: 20px + device-tip.label-width; y: tooltip-id-label.y;
width: parent.width - self.x - 12px; height: 16px;
text: device-tip.device-id;
color: WorkbenchStyle.text; font-size: 11px; font-weight: 600;
horizontal-alignment: right; vertical-alignment: center;
}
tooltip-group-label := Text {
x: 12px; y: tooltip-groups.y;
width: device-tip.label-width; height: 16px;
text: GalleryStrings.tr("分组", "Groups", "Группы");
color: WorkbenchStyle.muted; font-size: 10px;
vertical-alignment: center;
}
tooltip-groups := Text {
x: 20px + device-tip.label-width; y: tooltip-id-label.y + tooltip-id-label.height + 4px;
width: parent.width - self.x - 12px; height: max(16px, self.preferred-height);
text: device-tip.group-names == "" ? GalleryStrings.tr("未分组", "Ungrouped", "Без группы") : device-tip.group-names;
color: device-tip.group-names == "" ? WorkbenchStyle.muted : WorkbenchStyle.accent;
font-size: 10px;
wrap: word-wrap; horizontal-alignment: right; vertical-alignment: center;
}
}
// Dismiss the application menu when clicking anywhere outside of it.
if !root.gallery-mode && root.menu-open: TouchArea {
x: 0px;
y: 0px;
width: parent.width;
height: parent.height;
z: 9;
clicked => {
root.menu-open = false;
focus-sink.focus();
}
}
// Expand downward from the application toolbar.
if !root.gallery-mode && root.menu-open: Rectangle {
x: menu-button.x + menu-button.width - self.width;
y: menu-button.y + menu-button.height + 6px;
width: max(128px, max(menu-settings-label.preferred-width,
max(menu-messages-label.preferred-width + (root.announcement-unread > 0 ? menu-message-badge.width + 8px : 0px),
menu-about-label.preferred-width + (root.update-available ? menu-update-badge.width + 8px : 0px))) + 28px);
height: application-menu-content.preferred-height;
background: WorkbenchStyle.surface;
border-width: 1px;
border-color: WorkbenchStyle.control-border;
border-radius: UiRadius.menu;
z: 10;
TouchArea { }
application-menu-content := VerticalLayout {
padding: 6px;
spacing: 0px;
menu-settings := Rectangle {
height: 30px;
background: settings-touch.pressed ? WorkbenchStyle.pressed-surface
: settings-touch.has-hover ? WorkbenchStyle.hover-surface
: transparent;
border-width: 0px;
border-radius: UiRadius.control;
animate background { duration: 120ms; }
accessible-role: button;
accessible-label: UiStrings.settings;
accessible-action-default => { settings-touch.clicked(); }
menu-settings-label := Text {
x: 8px;
height: parent.height;
text: UiStrings.settings;
color: WorkbenchStyle.text; font-size: 12px;
vertical-alignment: center;
accessible-role: none;
}
settings-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.menu-open = false; root.settings-open = true; }
}
}
Rectangle {
height: 7px;
Rectangle {
x: 8px; y: 3px; width: parent.width - 16px; height: 1px;
background: WorkbenchStyle.border;
}
}
menu-messages := Rectangle {
height: 30px;
background: messages-touch.pressed ? WorkbenchStyle.pressed-surface
: messages-touch.has-hover ? WorkbenchStyle.hover-surface : transparent;
border-radius: UiRadius.control;
animate background { duration: 120ms; }
accessible-role: button;
accessible-label: MessageStrings.messages + (root.announcement-unread > 0 ? " · " + MessageStrings.unread + " " + root.announcement-unread : "");
accessible-action-default => { messages-touch.clicked(); }
menu-messages-label := Text {
x: 8px;
height: parent.height;
text: MessageStrings.messages;
color: WorkbenchStyle.text; font-size: 12px;
vertical-alignment: center;
accessible-role: none;
}
menu-message-badge := Rectangle {
visible: root.announcement-unread > 0;
x: parent.width - self.width - 7px;
y: (parent.height - self.height) / 2;
width: max(18px, menu-message-count.preferred-width + 10px);
height: 18px;
border-radius: 9px;
background: #fee4e2;
accessible-role: none;
menu-message-count := Text {
text: root.announcement-unread-label;
font-size: 10px; font-weight: 600; color: #b42318;
horizontal-alignment: center; vertical-alignment: center;
accessible-role: none;
}
}
messages-touch := TouchArea {
mouse-cursor: pointer;
clicked => {
root.menu-open = false;
root.messages-open = true;
root.refresh-announcements();
}
}
}
Rectangle {
height: 7px;
Rectangle {
x: 8px; y: 3px; width: parent.width - 16px; height: 1px;
background: WorkbenchStyle.border;
}
}
menu-about := Rectangle {
height: 30px;
background: about-touch.pressed ? WorkbenchStyle.pressed-surface
: about-touch.has-hover ? WorkbenchStyle.hover-surface
: transparent;
border-width: 0px;
border-radius: UiRadius.control;
animate background { duration: 120ms; }
accessible-role: button;
accessible-label: UiStrings.about + (root.update-available ? " · " + UiStrings.update-available : "");
accessible-action-default => { about-touch.clicked(); }
menu-about-label := Text {
x: 8px;
height: parent.height;
text: UiStrings.about;
color: WorkbenchStyle.text; font-size: 12px;
vertical-alignment: center;
accessible-role: none;
}
menu-update-badge := Rectangle {
visible: root.update-available;
x: parent.width - self.width - 7px;
y: (parent.height - self.height) / 2;
width: menu-update-label.preferred-width + 12px;
height: 18px;
border-radius: 9px;
background: #dbeafe;
accessible-role: none;
menu-update-label := Text {
text: UiStrings.new-version;
font-size: 11px; font-weight: 400; color: #1d4ed8;
horizontal-alignment: center; vertical-alignment: center;
accessible-role: none;
}
}
about-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.menu-open = false; root.about-open = true; }
}
}
}
}
if root.messages-open: Rectangle {
background: #15223850;
z: 40;
TouchArea { clicked => { focus-sink.focus(); } }
MessagePanel {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(592px, parent.width - 32px);
height: min(400px, parent.height - 24px);
items: root.announcements;
unread-count: root.announcement-unread; total: root.announcement-total;
loading: root.announcement-loading; failed: root.announcement-failed; loaded: root.announcement-loaded;
read-save-failed: root.announcement-read-save-failed;
delete-failed: root.announcement-delete-failed;
connected: root.signal-connected;
selected-id: root.announcement-selected-id; selected-title: root.announcement-selected-title;
selected-revision: root.announcement-selected-revision;
selected-body: root.announcement-selected-body; selected-date: root.announcement-selected-date;
selected-read: root.announcement-selected-read;
open-message(id, revision) => { root.open-announcement(id, revision); }
open-link(url) => { root.open-announcement-link(url); }
delete-message(id, revision) => { root.delete-announcement(id, revision); }
load-more => { root.load-more-announcements(); }
read-all => { root.read-all-announcements(); }
close => { root.messages-open = false; }
}
}
copy-timer := Timer {
interval: root.copy-feedback == CopyFeedback.failed ? 4s : 2500ms;
running: root.copy-feedback != CopyFeedback.idle;
triggered => { root.copy-feedback = CopyFeedback.idle; }
}
// Float above the workbench while keeping modal dialogs in front.
if !root.gallery-mode && root.copy-feedback != CopyFeedback.idle: WorkbenchNotice {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2 - 24px;
width-limit: min(440px, parent.width - 40px);
error: root.copy-feedback == CopyFeedback.failed;
icon: self.error ? FontAwesomeIcons.xmark : FontAwesomeIcons.check;
text: self.error
? GalleryStrings.tr("复制失败,请重试", "Couldn’t copy. Try again", "Не удалось скопировать. Повторите попытку")
: UiStrings.copied;
z: 30;
}
// Reset password modal.
if root.reset-password-open: Rectangle {
background: #00000042;
z: 40;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: root.compact-language ? 230px : 320px;
height: 170px;
VerticalLayout {
padding: 20px;
spacing: 12px;
DialogPromptText { prompt: UiStrings.new-password; }
HorizontalLayout {
alignment: center;
PasswordInput {
text <=> root.new-password-input;
max-length: 6;
}
}
Text { height: root.reset-password-invalid ? 18px : 0px; visible: root.reset-password-invalid; text: UiStrings.invalid-password; color: #d92d20; font-size: ImGuiFontStyle.small; horizontal-alignment: center; }
HorizontalLayout {
alignment: center; spacing: 8px;
PrimaryButton {
text: UiStrings.ok;
clicked => {
if (root.reset-password(root.new-password-input)) {
root.new-password-input = "";
root.reset-password-invalid = false;
root.reset-password-open = false;
} else { root.reset-password-invalid = true; }
}
}
SecondaryButton { text: UiStrings.cancel; clicked => { root.new-password-input = ""; root.reset-password-invalid = false; root.reset-password-open = false; } }
}
}
}
}
// One editor and one callback path for recent connections and DeskHub.
if root.device-group-open: DeviceGroupDialog {
z: 40;
device-id: root.device-group-id;
device-name: root.device-group-name;
options: root.device-group-options;
error: root.device-group-error;
new-group <=> root.device-group-new-name;
toggle-group(name) => { root.device-group-action("toggle", name); }
save(name) => {
if root.device-group-save(name) {
root.device-group-action("close", "");
root.device-group-open = false;
}
}
cancel => { root.device-group-action("close", ""); root.device-group-open = false; }
}
// Recent connection alias modal.
if root.alias-open: Rectangle {
background: #00000042;
z: 40;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: root.compact-language ? 230px : 320px;
height: 160px;
VerticalLayout {
padding: 20px; spacing: 14px;
DialogPromptText { prompt: UiStrings.edit-alias; }
RoundedLineEdit {
height: 28px;
normal-border-color: ImGuiLineStyle.border;
text <=> root.alias-input;
accepted => {
root.recent-edit-alias(root.alias-remote-id, root.alias-input);
root.alias-open = false;
}
}
HorizontalLayout {
alignment: center; spacing: 8px;
PrimaryButton { text: UiStrings.ok; clicked => { root.recent-edit-alias(root.alias-remote-id, root.alias-input); root.alias-open = false; } }
SecondaryButton { text: UiStrings.cancel; clicked => { root.alias-open = false; } }
}
}
}
}
// Delete confirmation modal.
if root.delete-open: Rectangle {
background: #00000042;
z: 40;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: root.compact-language ? 230px : 320px;
height: 140px;
VerticalLayout {
padding: 20px; spacing: 18px;
DialogPromptText { prompt: root.delete-from-gallery ? (root.gallery-group-index > 0 ? GalleryStrings.tr("从当前分组移除此设备?", "Remove from this group?", "Убрать из этой группы?") : GalleryStrings.tr("从桌面墙移除此设备?", "Remove from DeskHub?", "Убрать из DeskHub?")) : UiStrings.confirm-delete; }
HorizontalLayout {
alignment: center; spacing: 8px;
PrimaryButton { text: UiStrings.ok; clicked => { if root.delete-from-gallery { root.gallery-action("remove", root.delete-remote-id); } else { root.recent-delete(root.delete-remote-id); } root.delete-open = false; } }
SecondaryButton { text: UiStrings.cancel; clicked => { root.delete-open = false; } }
}
}
}
}
// Offline/error warning modal.
if root.offline-warning != "": Rectangle {
background: #00000042;
z: 39;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 270px; height: 150px;
VerticalLayout {
padding: 20px; spacing: 18px;
DialogPromptText { prompt: root.offline-warning; }
PrimaryButton { text: UiStrings.ok; clicked => { root.offline-warning = ""; } }
}
}
}
// Connection feedback follows the workbench spacing and action hierarchy.
if root.connection-dialog-open: Rectangle {
background: #15223850;
z: 48;
TouchArea { clicked => { focus-sink.focus(); } }
WorkbenchCard {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: 340px;
height: root.connection-password-required && !root.connection-validating ? 236px : 168px;
Text {
x: 24px; y: 24px; width: parent.width - 48px; height: 46px;
text: root.connection-status-text;
color: WorkbenchStyle.text; font-size: 14px;
wrap: word-wrap; horizontal-alignment: center; vertical-alignment: center;
}
if root.connection-password-required && !root.connection-validating: PasswordInput {
x: (parent.width - self.width) / 2; y: 86px;
width: 164px; height: 36px; text-size: 16px;
border-radius: UiRadius.control; border-color: WorkbenchStyle.control-border;
text <=> root.connection-password;
accepted => { root.connection-submit-password(root.connection-password, root.connection-remember-password); }
}
if root.connection-password-required && !root.connection-validating: Rectangle {
x: (parent.width - self.width) / 2; y: 134px;
width: remember-label.preferred-width + 29px; height: 21px;
ImGuiCheckBox { x: 0px; y: 0px; checked <=> root.connection-remember-password; }
remember-label := Text {
x: 29px; height: parent.height;
text: UiStrings.remember-password;
color: WorkbenchStyle.muted; font-size: 12px; vertical-alignment: center;
}
TouchArea {
x: 29px; width: parent.width - 29px; height: parent.height;
clicked => { root.connection-remember-password = !root.connection-remember-password; }
}
}
HorizontalLayout {
x: 24px; y: parent.height - 60px; width: parent.width - 48px; height: 36px;
spacing: 12px; alignment: center;
if root.connection-password-required || root.connection-pending: WorkbenchButton {
text: UiStrings.cancel;
clicked => { root.connection-cancel(); }
}
if root.connection-password-required && !root.connection-validating: WorkbenchButton {
primary: true; text: UiStrings.ok;
clicked => { root.connection-submit-password(root.connection-password, root.connection-remember-password); }
}
if !root.connection-password-required && !root.connection-pending: WorkbenchButton {
primary: true; text: UiStrings.ok;
clicked => { root.connection-acknowledge(); }
}
}
}
}
// macOS permission gate. On other platforms this remains hidden.
if root.permission-dialog-open: Rectangle {
background: #15223850;
z: 50;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(400px, parent.width - 40px);
height: permission-content.preferred-height;
background: #f8fafc; border-color: WorkbenchStyle.separator;
drop-shadow-color: #14294424; drop-shadow-blur: 24px; drop-shadow-offset-y: 8px;
TouchArea { clicked => { focus-sink.focus(); } }
permission-content := VerticalLayout {
padding: 24px; spacing: 20px;
VerticalLayout {
spacing: 8px;
Text {
text: UiStrings.request-permissions;
font-size: 20px; font-weight: 600; color: WorkbenchStyle.text;
wrap: word-wrap;
}
Text {
text: UiStrings.permission-required;
font-size: 12px; color: WorkbenchStyle.muted; wrap: word-wrap;
}
}
VerticalLayout {
spacing: 12px;
for permission[index] in [
{label: UiStrings.accessibility-permission, granted: root.accessibility-granted,
description: GalleryStrings.tr("允许远程键盘和鼠标控制", "Allow remote keyboard and mouse input", "Разрешить удалённый ввод с клавиатуры и мыши")},
{label: UiStrings.screen-recording-permission, granted: root.screen-recording-granted,
description: GalleryStrings.tr("允许共享此 Mac 的屏幕画面", "Allow sharing this Mac’s screen", "Разрешить показ экрана этого Mac")},
]: WorkbenchCard {
height: max(80px, permission-labels.preferred-height + 32px);
permission-labels := VerticalLayout {
x: 16px; y: 16px; width: parent.width - permission-action.width - 48px;
height: parent.height - 32px; spacing: 6px; alignment: center;
Text {
text: permission.label; font-size: 13px; font-weight: 600;
color: WorkbenchStyle.text; wrap: word-wrap;
}
Text {
text: permission.description; font-size: 11px;
color: WorkbenchStyle.muted; wrap: word-wrap;
}
}
permission-action := Rectangle {
x: parent.width - self.width - 16px; y: (parent.height - self.height) / 2;
width: max(96px, permission-action-metrics.preferred-width + 24px);
height: 36px;
permission-action-metrics := Text {
visible: false; font-size: 13px; font-weight: 600;
text: GalleryStrings.tr("前往设置", "Open settings", "Настройки");
}
if !permission.granted: WorkbenchButton {
width: parent.width; height: parent.height; primary: true;
text: permission-action-metrics.text;
accessible-label: permission.label + ": " + self.text;
clicked => {
if index == 0 { root.request-accessibility-permission(); }
else { root.request-screen-recording-permission(); }
}
}
if permission.granted: HorizontalLayout {
alignment: center; spacing: 6px;
Text {
text: FontAwesomeIcons.check; font-family: "Font Awesome 6 Free";
font-weight: 900; font-size: 12px; color: WorkbenchStyle.success;
vertical-alignment: center; accessible-role: none;
}
Text {
text: GalleryStrings.tr("已授权", "Granted", "Разрешено");
font-size: 12px; color: WorkbenchStyle.success; vertical-alignment: center;
}
}
}
}
}
VerticalLayout {
spacing: 12px;
Rectangle { height: 1px; background: WorkbenchStyle.separator; }
Text {
text: GalleryStrings.tr("请在系统设置中允许 CrossDesk 访问。如系统提示重新打开应用,请按提示操作。",
"Allow CrossDesk access in System Settings. Reopen the app if macOS asks you to.",
"Разрешите доступ для CrossDesk в Системных настройках. Перезапустите приложение, если macOS попросит об этом.");
font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap;
}
}
}
}
}
// Both tabs share one draft and the same save/cancel actions.
if root.settings-open: Rectangle {
background: #15223850;
z: 45;
TouchArea { clicked => { focus-sink.focus(); } }
settings-dialog := DialogSurface {
// Center every content section; the scrollbar sits in the right margin.
private property <length> content-inset: 26px;
private property <length> scrollbar-gutter: 22px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(400px, parent.width - 40px);
height: min(446px, parent.height - 16px);
background: #f8fafc; border-radius: UiRadius.surface; border-color: #e1e7ef;
drop-shadow-color: #14294424; drop-shadow-blur: 24px; drop-shadow-offset-y: 8px;
TouchArea { clicked => { focus-sink.focus(); } }
Text {
x: settings-dialog.content-inset; y: 18px;
width: parent.width - 2 * settings-dialog.content-inset; height: 27px;
text: UiStrings.settings; font-size: 20px; font-weight: 600; color: #22324a;
}
Rectangle {
x: settings-dialog.content-inset; y: 56px;
width: parent.width - 2 * settings-dialog.content-inset; height: 40px;
background: WorkbenchStyle.canvas; border-radius: UiRadius.menu;
border-width: 1px; border-color: WorkbenchStyle.border;
Rectangle {
x: 4px + root.settings-tab * (parent.width - 4px) / 2;
y: 4px; width: (parent.width - 12px) / 2; height: parent.height - 8px;
background: WorkbenchStyle.surface; border-radius: UiRadius.control;
border-width: 1px; border-color: WorkbenchStyle.control-border;
animate x { duration: 140ms; easing: ease-out; }
}
for label[index] in [GalleryStrings.tr("常规", "General", "Общие"), GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub")]: Rectangle {
x: 4px + index * (parent.width - 4px) / 2;
y: 4px; width: (parent.width - 12px) / 2; height: parent.height - 8px;
border-radius: UiRadius.control;
background: root.settings-tab == index ? transparent
: tab-touch.pressed ? WorkbenchStyle.pressed-surface
: tab-touch.has-hover ? WorkbenchStyle.hover-surface : transparent;
animate background { duration: 100ms; }
accessible-role: button; accessible-label: label;
accessible-action-default => { tab-touch.clicked(); }
Text {
text: label; font-size: 12px; font-weight: root.settings-tab == index ? 600 : 500;
color: root.settings-tab == index ? WorkbenchStyle.accent : WorkbenchStyle.muted;
horizontal-alignment: center; vertical-alignment: center;
}
tab-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.settings-tab = index; focus-sink.focus(); }
}
}
}
if root.settings-tab == 0: settings-scroll := ScrollView {
x: settings-dialog.content-inset; y: 108px;
width: parent.width - 2 * settings-dialog.content-inset + settings-dialog.scrollbar-gutter;
height: parent.height - 174px;
viewport-width: self.visible-width;
viewport-height: settings-content.preferred-height;
viewport-y <=> root.settings-scroll-y;
horizontal-scrollbar-policy: always-off;
vertical-scrollbar-policy: as-needed;
// Fluent's scrollbar expands over the viewport on hover.
// Keep its full 14px track plus an 8px gap outside the groups.
Rectangle {
x: 0px; y: 0px;
width: settings-scroll.viewport-width;
height: settings-scroll.viewport-height;
settings-content := VerticalLayout {
padding: 0px; padding-right: settings-dialog.scrollbar-gutter; padding-bottom: 4px;
spacing: 14px;
SettingsGroup {
title: GalleryStrings.tr("界面", "Interface", "Интерфейс"); rows: 1;
SettingsRow {
label: UiStrings.language; divider: false;
SettingsSelect { x: parent.width - 116px; y: 4px; width: 104px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
}
}
SettingsGroup {
title: GalleryStrings.tr("视频", "Video", "Видео");
rows: root.capture-method-visible ? 3 : 2;
if root.capture-method-visible: SettingsRow {
label: UiStrings.capture-method; enabled: !root.settings-session-active;
SettingsSelect { x: parent.width - 116px; y: 4px; width: 104px; enabled: !root.settings-session-active; model: [UiStrings.capture-method-auto, "DXGI", "WGC", "GDI"]; current-index <=> root.capture-method-index; }
}
SettingsRow {
label: UiStrings.codec; enabled: !root.settings-session-active;
SettingsSelect { x: parent.width - 116px; y: 4px; width: 104px; enabled: !root.settings-session-active; model: [UiStrings.codec-h264, UiStrings.codec-av1]; current-index <=> root.codec-index; }
}
SettingsToggleRow { label: UiStrings.hardware-codec; checked <=> root.hardware-codec-enabled; enabled: root.hardware-codec-available && !root.settings-session-active; divider: false; }
}
SettingsGroup {
title: GalleryStrings.tr("网络连接", "Network", "Сеть"); rows: 3;
SettingsToggleRow { label: UiStrings.turn-relay; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
SettingsToggleRow { label: UiStrings.force-relay; checked <=> root.force-relay-enabled; enabled: root.turn-enabled && !root.settings-session-active; }
SettingsToggleRow {
checked <=> root.self-hosted-enabled; enabled: !root.settings-session-active; divider: false;
SettingsButton { x: 0px; y: 2px; width: parent.width - 60px; height: 32px; quiet: true; align-left: true; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
}
}
SettingsGroup {
title: GalleryStrings.tr("隐私保护", "Privacy", "Приватность"); rows: 1;
SettingsToggleRow { label: UiStrings.privacy-on-connect; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; divider: false; }
}
SettingsGroup {
title: GalleryStrings.tr("本地存储", "Local storage", "Локальное хранение"); rows: 2;
SettingsToggleRow { label: UiStrings.save-remote-previews; checked <=> root.save-remote-previews; }
SettingsRow {
label: UiStrings.file-save-path; enabled: !root.settings-session-active; divider: false;
SettingsButton { x: parent.width - 116px; y: 4px; width: 104px; height: 28px; text: root.file-save-path == "" ? UiStrings.default-desktop : root.file-save-path; enabled: !root.settings-session-active; clicked => { root.browse-save-path(); } }
}
}
SettingsGroup {
title: GalleryStrings.tr("启动与后台", "Startup & background", "Запуск и фоновая работа"); rows: 2;
if !root.autostart-system-managed: SettingsToggleRow { label: UiStrings.autostart; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
if root.autostart-system-managed: SettingsToggleRow {
label: UiStrings.autostart;
checked: root.autostart-status == AutostartStatus.enabled;
toggle-on-click: false;
clicked => { root.autostart-guide-open = true; }
}
SettingsToggleRow { label: UiStrings.daemon; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; divider: false; }
}
}
}
}
if root.settings-tab == 1: GallerySettings {
x: settings-dialog.content-inset; y: 108px;
width: parent.width - 2 * settings-dialog.content-inset; height: parent.height - 174px;
resolution-index <=> root.gallery-resolution-draft;
frame-rate-index <=> root.gallery-frame-rate-draft;
bandwidth-index <=> root.gallery-bandwidth-draft;
error: root.gallery-settings-error;
}
Rectangle {
x: settings-dialog.content-inset; y: parent.height - 64px;
width: parent.width - 2 * settings-dialog.content-inset; height: 1px; background: #e6ebf2;
}
HorizontalLayout {
x: settings-dialog.content-inset; y: parent.height - 50px;
width: parent.width - 2 * settings-dialog.content-inset; height: 32px; spacing: 10px;
Rectangle { horizontal-stretch: 1; }
SettingsButton {
width: 80px; text: UiStrings.cancel;
clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; }
}
SettingsButton {
width: 92px; primary: true;
text: GalleryStrings.tr("保存", "Save", "Сохранить");
clicked => { if root.save-settings() { root.settings-open = false; root.self-host-settings-open = false; } }
}
}
}
}
if root.autostart-guide-open && root.settings-open: Rectangle {
background: #15223835; z: 49;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2;
width: min(460px, parent.width - 32px);
height: min(380px, parent.height - 24px);
background: #f8fafc; border-color: #e1e7ef;
drop-shadow-color: #14294424; drop-shadow-blur: 18px; drop-shadow-offset-y: 6px;
TouchArea { clicked => { focus-sink.focus(); } }
Text {
x: 24px; y: 18px; width: parent.width - 48px; height: 26px;
text: GalleryStrings.tr("设置开机自启动", "Set up auto start", "Настройка автозапуска");
font-size: 18px; font-weight: 600; color: WorkbenchStyle.text;
}
ScrollView {
x: 24px; y: 58px; width: parent.width - 48px; height: parent.height - 120px;
viewport-width: self.visible-width;
horizontal-scrollbar-policy: always-off;
viewport-height: guide-content.preferred-height;
guide-content := VerticalLayout {
width: parent.width; padding: 0px; spacing: 12px;
Text { text: root.autostart-status-text; color: WorkbenchStyle.accent; font-size: 13px; wrap: word-wrap; }
Text {
text: root.autostart-status == AutostartStatus.unsupported ? GalleryStrings.tr(
"当前设备不支持开机自启动。", "This device does not support auto start.", "Это устройство не поддерживает автозапуск.") :
root.autostart-status == AutostartStatus.failed ? GalleryStrings.tr(
"无法读取系统状态。请刷新重试,或在系统设置中核对。", "Could not read the system status. Retry or check it in Settings.", "Не удалось прочитать состояние. Повторите попытку или проверьте настройки системы.") :
GalleryStrings.tr("请在系统设置中添加或移除本应用,返回后会自动刷新状态。", "Add or remove this app in system Settings. The status refreshes when you return.", "Добавьте или удалите приложение в настройках системы. После возвращения статус обновится.");
color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap;
}
if root.autostart-status != AutostartStatus.unsupported: Text {
text: GalleryStrings.tr(
"设置 → 应用和元服务 → 应用启动管理\n\n开启:点击“添加应用”,选择 CrossDesk,然后点击“完成”。\n\n关闭:在自启动列表中找到本应用,点击“移除”。",
"Settings → Apps & services → App startup management\n\nEnable: choose Add app, select CrossDesk, then choose Done.\n\nDisable: find this app in the startup list and choose Remove.",
"Настройки → Приложения и сервисы → Управление запуском\n\nВключить: добавьте CrossDesk в список и подтвердите выбор.\n\nВыключить: удалите приложение из списка автозапуска.");
color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap;
}
}
}
SettingsButton {
x: 24px; y: parent.height - 48px; width: 112px;
text: GalleryStrings.tr("刷新状态", "Refresh status", "Обновить");
enabled: root.autostart-status != AutostartStatus.checking;
clicked => { root.refresh-autostart-status(); }
}
SettingsButton {
x: parent.width - 116px; y: parent.height - 48px; width: 92px; primary: true;
text: UiStrings.ok; clicked => { root.autostart-guide-open = false; }
}
}
}
if root.self-host-settings-open: Rectangle {
background: #15223835;
z: 49;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: 360px; height: root.self-host-settings-error == "" ? 266px : 322px;
border-radius: UiRadius.surface; border-color: #e1e7ef; background: #f8fafc;
drop-shadow-color: #14294424; drop-shadow-blur: 18px; drop-shadow-offset-y: 6px;
TouchArea { clicked => { focus-sink.focus(); } }
Text {
x: 18px; y: 18px; width: parent.width - 36px; height: 24px;
text: UiStrings.self-hosted-settings; font-size: 16px; font-weight: 600; color: #22324a;
}
Text { x: 18px; y: 56px; width: parent.width - 36px; height: 20px; text: UiStrings.server-host; color: #34445a; font-size: 12px; overflow: elide; vertical-alignment: center; }
RoundedLineEdit {
x: 18px; y: 80px; width: parent.width - 36px; height: 30px;
surrounding-background: #f8fafc;
normal-border-color: WorkbenchStyle.control-border;
text <=> root.server-host;
}
Text { x: 18px; y: 120px; width: parent.width - 36px; height: 20px; text: UiStrings.server-port; color: #34445a; font-size: 12px; overflow: elide; vertical-alignment: center; }
RoundedLineEdit {
x: 18px; y: 144px; width: parent.width - 36px; height: 30px;
surrounding-background: #f8fafc;
normal-border-color: WorkbenchStyle.control-border;
text <=> root.server-port; input-type: InputType.number;
}
if root.self-host-settings-error != "": Text {
x: 18px; y: 186px; width: parent.width - 36px; height: 48px;
text: root.self-host-settings-error; color: #c53e3e; font-size: 11px; wrap: word-wrap;
}
Rectangle { x: 18px; y: parent.height - 66px; width: parent.width - 36px; height: 1px; background: #e6ebf2; }
SettingsButton { x: parent.width - 114px; y: parent.height - 50px; width: 92px; primary: true; text: UiStrings.ok; clicked => { if root.save-self-hosted-settings() { root.self-host-settings-open = false; } } }
SettingsButton { x: parent.width - 204px; y: parent.height - 50px; width: 80px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } }
}
}
if root.about-open: Rectangle {
background: #15223850;
z: 46;
TouchArea { clicked => { focus-sink.focus(); } }
SettingsDialog {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(480px, parent.width - 40px);
// The overlay excludes any custom titlebar on every platform.
min-height: 0px; max-height: parent.height - 32px;
// Keep the dialog stable as update status and release details change.
height: min(488px, parent.height - 32px);
title: UiStrings.about;
close-label: MessageStrings.close;
close => { root.about-open = false; }
// Preserve section spacing and keep actions visible when longer
// translations need scrolling inside a small window.
about-scroll := ScrollView {
x: 24px; y: parent.content-top;
width: parent.width - 48px + 22px;
height: about-footer.y - self.y - 16px;
viewport-width: self.visible-width;
viewport-height: about-content.preferred-height;
horizontal-scrollbar-policy: always-off;
vertical-scrollbar-policy: as-needed;
about-content := VerticalLayout {
min-width: 0px;
width: about-scroll.width; height: self.preferred-height;
// Keep the expanding scrollbar outside the content.
padding: 0px; padding-right: 22px; spacing: 16px;
Rectangle {
height: max(80px, product-info.preferred-height + 32px);
background: WorkbenchStyle.surface;
border-radius: UiRadius.surface;
border-width: 1px; border-color: WorkbenchStyle.border;
Image {
x: 16px; y: (parent.height - self.height) / 2;
width: 56px; height: 56px;
source: @image-url("../../../resources/linux/crossdesk_128x128.png");
image-fit: contain;
}
product-info := VerticalLayout {
x: 88px; y: 16px; width: parent.width - 104px; height: parent.height - 32px;
alignment: center; spacing: 5px;
Text { text: "CrossDesk"; font-size: 22px; font-weight: 600; color: WorkbenchStyle.text; }
Text {
text: UiStrings.version + " " + root.current-version;
color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap;
}
}
}
if root.update-available: Rectangle {
height: update-summary.preferred-height + 24px;
background: WorkbenchStyle.field-surface;
border-radius: UiRadius.control;
border-width: 1px;
border-color: WorkbenchStyle.separator;
update-summary := VerticalLayout {
x: 12px; y: 12px; width: parent.width - 24px;
height: self.preferred-height;
padding: 0px; spacing: 10px;
if root.update-available: HorizontalLayout {
spacing: 12px;
height: max(32px, version-link.preferred-height);
Rectangle {
min-width: 0px; horizontal-stretch: 1;
accessible-role: button;
accessible-label: UiStrings.release-notes + " " + root.latest-version;
accessible-action-default => { version-touch.clicked(); }
version-link := Text {
width: parent.width; height: parent.height;
text: UiStrings.update-available + ": " + root.latest-version;
color: version-touch.has-hover ? #254cc0 : WorkbenchStyle.accent;
font-size: 13px; font-weight: 500; wrap: word-wrap;
vertical-alignment: center;
}
version-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.about-open = false; root.update-open = true; }
}
}
SettingsButton {
width: max(116px, release-notes-button-metrics.preferred-width + 24px);
text: UiStrings.view-release-notes;
clicked => { root.about-open = false; root.update-open = true; }
release-notes-button-metrics := Text { visible: false; text: UiStrings.view-release-notes; font-size: 12px; }
}
}
}
}
SponsorshipSection {
open-afdian => { root.open-afdian-sponsor(); }
open-kofi => { root.open-kofi-sponsor(); }
open-github => { root.open-github-sponsor(); }
}
VerticalLayout {
spacing: 5px;
Text { text: UiStrings.copyright; color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap; }
Text { text: UiStrings.license; color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap; }
}
}
}
about-footer := VerticalLayout {
x: 24px; y: parent.height - self.height - 24px;
width: parent.width - 48px; height: self.preferred-height;
spacing: 16px;
Rectangle { height: 1px; background: WorkbenchStyle.separator; }
HorizontalLayout {
alignment: end; spacing: 8px;
// Status updates use the spare footer space without reflowing the content.
Text {
min-width: 0px; height: 32px; horizontal-stretch: 1;
text: root.update-status-text;
color: root.update-check-status == UpdateCheckStatus.failed ? #b3261e
: root.update-check-status == UpdateCheckStatus.up-to-date ? WorkbenchStyle.success : WorkbenchStyle.muted;
font-size: 12px; wrap: word-wrap; overflow: elide;
vertical-alignment: center;
}
SettingsButton {
width: max(116px, check-button-metrics.preferred-width + 24px);
text: UiStrings.check-for-updates;
enabled: root.update-check-status != UpdateCheckStatus.checking;
clicked => { root.check-for-updates(); }
check-button-metrics := Text { visible: false; text: UiStrings.check-for-updates; font-size: 12px; }
}
SettingsButton { width: 80px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } }
}
}
}
}
// Update notification modal.
if root.update-open && root.update-available: Rectangle {
background: #14233b45;
z: 47;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(480px, parent.width - 40px);
// Use the same content area for sizing and centering the dialog.
height: min(488px, parent.height - 32px);
border-radius: UiRadius.surface;
border-color: WorkbenchStyle.border;
drop-shadow-color: #07142b24;
drop-shadow-blur: 24px;
drop-shadow-offset-y: 8px;
VerticalLayout {
padding: 24px;
spacing: 16px;
VerticalLayout {
vertical-stretch: 0;
spacing: 6px;
Text {
height: 24px;
text: UiStrings.update-available;
color: WorkbenchStyle.text;
font-size: 18px;
font-weight: 600;
vertical-alignment: center;
}
HorizontalLayout {
height: 20px;
spacing: 12px;
Text {
text: root.latest-version;
color: WorkbenchStyle.muted;
font-size: 13px;
vertical-alignment: center;
}
Rectangle { horizontal-stretch: 1; }
Rectangle {
width: update-website-label.preferred-width;
accessible-role: button;
accessible-label: update-website-label.text;
accessible-action-default => { root.open-download(); }
update-website-label := Text {
text: UiStrings.access-website + "https://crossdesk.cn";
color: website-touch.has-hover ? #254cc0 : WorkbenchStyle.accent;
font-size: 13px;
vertical-alignment: center;
}
website-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } }
}
}
}
VerticalLayout {
vertical-stretch: 1;
spacing: 8px;
Text {
height: 18px;
text: UiStrings.release-notes;
color: WorkbenchStyle.text;
font-size: 12px;
font-weight: 600;
vertical-alignment: center;
}
Rectangle {
background: WorkbenchStyle.field-surface;
border-width: 1px;
border-color: WorkbenchStyle.separator;
border-radius: UiRadius.control;
// Inset the viewport so text and the scrollbar stay clear
// of the rounded frame, including at either scroll limit.
update-notes-scroll := ScrollView {
x: 12px;
y: 12px;
width: parent.width - 24px;
height: parent.height - 24px;
viewport-width: self.width - 12px;
viewport-height: update-notes-content.preferred-height;
horizontal-scrollbar-policy: always-off;
vertical-scrollbar-policy: as-needed;
update-notes-content := VerticalLayout {
width: update-notes-scroll.viewport-width;
alignment: start;
spacing: 8px;
padding-bottom: 6px;
for block in root.release-note-blocks: Rectangle {
height: update-note.preferred-height + (block.section-gap ? 6px : 0px);
update-note := StyledText {
y: block.section-gap ? 6px : 0px;
// Measure wrapped paragraphs at the viewport
// width before stacking the following block.
width: update-notes-scroll.viewport-width;
height: self.preferred-height;
text: block.content;
default-color: WorkbenchStyle.text;
default-font-size: 13px;
// Match the 1.5em line height of message bodies.
default-font-family: "CrossDesk Message Spacing";
}
}
}
}
}
}
SponsorshipSection {
open-afdian => { root.open-afdian-sponsor(); }
open-kofi => { root.open-kofi-sponsor(); }
open-github => { root.open-github-sponsor(); }
}
VerticalLayout {
vertical-stretch: 0;
spacing: 16px;
Rectangle { height: 1px; background: WorkbenchStyle.separator; }
HorizontalLayout {
alignment: end;
spacing: 8px;
WorkbenchButton {
width: 88px;
height: 34px;
text: UiStrings.later;
clicked => { root.update-open = false; }
}
WorkbenchButton {
width: 88px;
height: 34px;
primary: true;
text: UiStrings.download;
clicked => { root.open-download(); root.update-open = false; }
}
}
}
}
}
}
// Linux Wayland sessions show this advisory once per application launch.
if root.wayland-notice-open: Rectangle {
background: #14233b66;
z: 50;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: 512px;
height: wayland-notice-content.preferred-height;
border-radius: UiRadius.surface;
border-color: #dce3ee;
drop-shadow-color: #07142b30;
drop-shadow-blur: 28px;
drop-shadow-offset-y: 10px;
wayland-notice-content := VerticalLayout {
padding: 24px; spacing: 18px;
VerticalLayout {
spacing: 7px;
Text {
text: UiStrings.wayland-notice-title;
color: #1c2b43;
font-size: 20px;
font-weight: 700;
wrap: word-wrap;
}
Text {
text: UiStrings.wayland-notice-subtitle;
color: #68758a;
font-size: ImGuiFontStyle.base;
wrap: word-wrap;
}
}
Rectangle {
background: #fff8eb;
border-color: #f0dfba;
border-width: 1px;
border-radius: UiRadius.surface;
VerticalLayout {
padding: 14px; spacing: 8px;
Text {
text: UiStrings.wayland-permission-title;
color: #805719;
font-size: 13px;
font-weight: 600;
wrap: word-wrap;
}
Text {
text: UiStrings.wayland-notice-message;
color: #6c5a3d;
font-size: ImGuiFontStyle.base;
wrap: word-wrap;
}
}
}
Rectangle {
background: #f3f7fd;
border-color: #dfe8f5;
border-width: 1px;
border-radius: UiRadius.surface;
VerticalLayout {
padding: 14px; spacing: 8px;
Text {
text: UiStrings.wayland-recommendation-title;
color: #244c87;
font-size: 13px;
font-weight: 600;
wrap: word-wrap;
}
Text {
text: UiStrings.wayland-recommendation-message;
color: #59677c;
font-size: ImGuiFontStyle.base;
wrap: word-wrap;
}
}
}
Rectangle { height: 1px; background: #e9edf3; }
HorizontalLayout {
height: 34px; alignment: end;
PrimaryButton {
width: 104px; height: 34px;
border-radius: UiRadius.control;
text-size: ImGuiFontStyle.base;
text: UiStrings.wayland-notice-acknowledge;
accessible-role: button;
accessible-label: UiStrings.wayland-notice-acknowledge;
accessible-action-default => { root.wayland-notice-open = false; }
clicked => { root.wayland-notice-open = false; }
}
}
}
}
}
// Portable Windows builds prompt for the optional lock-screen service.
if root.portable-service-dialog-open: Rectangle {
background: #00000052;
z: 50;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 480px; height: 300px;
VerticalLayout {
padding: 24px; spacing: 14px;
DialogPromptText {
prompt: UiStrings.service-setup-title;
text-color: #242832;
text-size: ImGuiFontStyle.prominent;
text-weight: 600;
}
Rectangle {
background: #f7f8fa; border-color: #d9dde4; border-width: 1px; border-radius: UiRadius.surface;
Text { x: 14px; y: 22px; width: parent.width - 28px; height: parent.height - 24px; text: UiStrings.service-setup-message; color: #3d434d; wrap: word-wrap; vertical-alignment: center; }
}
if root.portable-service-status != "": Text {
text: root.portable-service-status;
color: root.portable-service-succeeded ? #25874b : root.portable-service-installing ? #68707d : #b23b3b;
horizontal-alignment: center;
wrap: word-wrap;
}
if !root.portable-service-succeeded: HorizontalLayout {
height: 26px; alignment: center; spacing: 8px;
Text { text: UiStrings.do-not-remind; vertical-alignment: center; color: #4a505a; }
ToggleSwitch { checked <=> root.portable-service-do-not-remind; enabled: !root.portable-service-installing; }
}
HorizontalLayout {
height: 34px; alignment: center; spacing: 8px;
if root.portable-service-succeeded: PrimaryButton { text: UiStrings.ok; clicked => { root.acknowledge-portable-service(); } }
if !root.portable-service-succeeded: PrimaryButton {
text: UiStrings.install-service;
enabled: !root.portable-service-installing;
clicked => { root.install-portable-service(); }
}
if !root.portable-service-succeeded: SecondaryButton {
text: UiStrings.cancel;
enabled: !root.portable-service-installing;
clicked => { root.cancel-portable-service(); }
}
}
}
}
}
if root.portable-service-suppressed-notice-open: Rectangle {
background: #00000052;
z: 51;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 440px; height: 190px;
VerticalLayout {
padding: 24px; spacing: 16px;
DialogPromptText {
prompt: UiStrings.notification;
text-color: #242832;
text-size: ImGuiFontStyle.prominent;
text-weight: 600;
}
DialogPromptText {
prompt: UiStrings.service-suppressed-message;
text-color: #3d434d;
}
PrimaryButton { text: UiStrings.ok; clicked => { root.acknowledge-portable-service-suppressed(); } }
}
}
}
}
if root.gallery-mode && root.gallery-network-open: GalleryNetworkDialog {
x: 0px; y: root.titlebar-height;
width: root.width; height: root.height - self.y;
z: 100;
devices: root.gallery-network-devices;
close => { root.gallery-network-open = false; focus-sink.focus(); }
}
// Ignore clicks against moving cards and duplicate fullscreen requests.
if root.gallery-transitioning: TouchArea {
x: 0px; y: root.titlebar-height;
width: root.width; height: root.height - self.y;
z: 250;
}
if root.custom-titlebar: Rectangle {
x: 0px;
y: 0px;
width: root.width;
height: root.height;
background: transparent;
border-width: 1px;
border-color: root.wayland-titlebar ? #aaa8a6 : #8b8b8b;
border-radius:
root.wayland-titlebar ? root.wayland-corner-radius : 0px;
z: 200;
}
}
}