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

2387 lines
121 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 } from "workbench_controls.slint";
import { GalleryStrings } from "device_strings.slint";
import { DeviceCardActions, DeviceCardStyle } from "device_card_actions.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 { 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, MiniToggleSwitch, CompactButton, PrimaryButton, SecondaryButton, ToggleSwitch, SectionTitle, DialogSurface, ImGuiLineStyle, ImGuiFontStyle } from "common.slint";
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 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> 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: ";
}
// 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-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 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-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 save-self-hosted-settings() -> bool;
callback cancel-self-hosted-settings;
callback browse-save-path;
callback open-download;
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 <bool> copy-toast: false;
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.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", "");
focus-sink.focus();
}
}
changed wayland-notice-open => {
if root.wayland-notice-open { focus-sink.focus(); }
}
changed messages-open => {
if root.messages-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 => {
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 => { 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.wayland-notice-open {
if event.text == Key.Return || event.text == Key.Escape {
root.wayland-notice-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 { 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; }
WorkbenchButton {
x: parent.width - 42px; y: 2px; width: 36px; quiet: true;
icon: root.copy-toast ? FontAwesomeIcons.check : FontAwesomeIcons.copy;
tooltip: root.copy-toast ? UiStrings.copied : GalleryStrings.tr("复制本机 ID", "Copy local ID", "Копировать ID");
clicked => { if root.copy-local-id() { root.copy-toast = true; 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 = ""; remote-id-format-timer.restart(); }
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: horizontal cards with thumbnail and hover 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; }
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> toolbar-width: DeviceCardStyle.toolbar-width;
private property <length> toolbar-right-padding: 3px;
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: 0px;
y: 80px;
width: parent.width;
height: 44px;
background: white;
Rectangle { x: 0px; y: 0px; width: parent.width; height: 1px; background: WorkbenchStyle.border; }
Text {
x: 12px; y: 4px; height: 19px;
width: parent.width - 24px;
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 - 92px;
text: connection.remote-id; font-size: 11px; color: WorkbenchStyle.muted;
overflow: elide;
}
presence-label := Text {
x: parent.width - self.preferred-width - 12px; y: 24px; height: 16px;
text: connection-card.available ? UiStrings.online : UiStrings.offline;
font-size: 10px; color: connection-card.available ? WorkbenchStyle.success : WorkbenchStyle.muted;
}
}
// 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.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.recent-connect(connection.remote-id); }
function show-details() {
if !self.has-hover { 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(); }
}
if card-touch.has-hover: DeviceCardActions {
x: parent.width - self.width - connection-card.toolbar-right-padding;
y: 52px;
remove-label: GalleryStrings.tr("删除记录", "Delete record", "Удалить запись");
connected: connection.connected;
edit-alias => {
root.alias-remote-id = connection.remote-id;
root.alias-input = connection.display-name;
root.alias-open = true;
}
edit-groups => { root.open-device-groups(connection.remote-id, connection.display-name); }
remove-device => {
root.delete-remote-id = connection.remote-id;
root.delete-from-gallery = false;
root.delete-open = true;
}
connect-device => { root.recent-connect(connection.remote-id); }
disconnect-device => { root.recent-disconnect(connection.remote-id); }
}
}
Rectangle {
width: parent.width; height: 124px;
border-width: 1px; border-radius: UiRadius.surface;
border-color: card-touch.has-hover ? 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.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 <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));
width: min(260px, parent.width - 16px);
height: (self.host-name != "" && self.host-name != self.device-name
? 80px : 64px) + tooltip-groups.preferred-height + 2px;
background: #22252bf2;
border-width: 1px;
border-color: #ffffff38;
border-radius: UiRadius.control;
z: 20;
VerticalLayout {
padding: 8px;
spacing: 2px;
Text {
text: UiStrings.device-name + ": " + device-tip.device-name;
color: white;
font-size: ImGuiFontStyle.small;
overflow: elide;
}
if device-tip.host-name != "" && device-tip.host-name != device-tip.device-name: Text {
text: device-tip.host-name;
color: #e1e4e8;
font-size: ImGuiFontStyle.small;
overflow: elide;
}
Text {
text: UiStrings.remote-id + ": " + device-tip.device-id;
color: #e1e4e8;
font-size: ImGuiFontStyle.small;
overflow: elide;
}
tooltip-groups := Text {
text: GalleryStrings.tr("分组", "Groups", "Группы") + ": " +
(device-tip.group-names == "" ? GalleryStrings.tr("未分组", "Ungrouped", "Без группы") : device-tip.group-names);
color: #e1e4e8;
font-size: ImGuiFontStyle.small;
wrap: word-wrap;
vertical-stretch: 0;
}
Text {
text: device-tip.online ? UiStrings.online : device-tip.presence-known ? UiStrings.offline
: GalleryStrings.tr("未知", "Unknown", "Неизвестно");
color: device-tip.online ? #62d98b : #d1d5db;
font-size: ImGuiFontStyle.small;
}
}
}
// 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: #00000042;
z: 40;
TouchArea { clicked => { focus-sink.focus(); } }
MessagePanel {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: 520px;
height: 330px;
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-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(); }
close => { root.messages-open = false; }
}
}
copy-timer := Timer {
interval: 1000ms;
triggered => { root.copy-toast = false; }
}
remote-id-format-timer := Timer {
interval: 1ms;
triggered => {
let formatted = root.format-remote-id(root.remote-id-input);
root.remote-id-input = formatted;
// IDs are ASCII-only, so character-count is also the UTF-8 offset
// expected by set-selection-offsets().
remote-id-editor.set-selection-offsets(
formatted.character-count, formatted.character-count);
}
}
if root.copy-toast: Rectangle {
x: (parent.width - self.width) / 2; y: parent.height - 92px; width: 280px; height: 40px;
background: #fffffff2;
border-radius: UiRadius.surface;
border-width: 1px;
border-color: ImGuiLineStyle.border;
z: 30;
Text { text: UiStrings.copied; color: #333840; horizontal-alignment: center; vertical-alignment: center; }
}
// 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: transparent;
z: 50;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: root.compact-language ? 130px : 260px;
height: 125px;
border-radius: UiRadius.surface;
Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: 21px;
background: #d3d3d3;
border-top-left-radius: UiRadius.surface;
border-top-right-radius: UiRadius.surface;
z: 5;
Text { x: 6px; text: UiStrings.request-permissions; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
}
Text { x: 10px; y: 32px; width: root.compact-language ? 110px : 240px; height: 24px; text: UiStrings.permission-required; color: #202124; font-size: 8px; wrap: word-wrap; }
Text { x: 10px; y: 61px; width: root.compact-language ? 78px : 198px; height: 18px; text: "1. " + UiStrings.accessibility-permission + ":"; color: #202124; font-size: 8px; overflow: elide; vertical-alignment: center; }
MiniToggleSwitch {
x: root.compact-language ? 90px : 210px;
y: 61px;
checked: root.accessibility-granted;
enabled: !root.accessibility-granted;
toggled(value) => { if (value) { root.request-accessibility-permission(); } }
}
Text { x: 10px; y: 85px; width: root.compact-language ? 78px : 198px; height: 18px; text: "2. " + UiStrings.screen-recording-permission + ":"; color: #202124; font-size: 8px; overflow: elide; vertical-alignment: center; }
MiniToggleSwitch {
x: root.compact-language ? 90px : 210px;
y: 85px;
checked: root.screen-recording-granted;
enabled: !root.screen-recording-granted;
toggled(value) => { if (value) { root.request-screen-recording-permission(); } }
}
}
}
// 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;
SettingsToggleRow { label: UiStrings.autostart; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
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.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: transparent;
z: 46;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: 320px;
height: max(216px, about-content.preferred-height);
border-radius: UiRadius.surface;
Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: 21px;
background: #d3d3d3;
border-top-left-radius: UiRadius.surface;
border-top-right-radius: UiRadius.surface;
z: 5;
Text { x: 6px; text: UiStrings.about; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
}
// Grow for update actions and wrapped translations while keeping
// the footer below the version and check result.
about-content := VerticalLayout {
padding: 16px;
padding-top: 35px;
spacing: 12px;
alignment: space-between;
VerticalLayout {
vertical-stretch: 0;
spacing: 4px;
Text {
text: "CrossDesk " + root.current-version;
color: #202124;
font-size: ImGuiFontStyle.base;
font-weight: 600;
min-height: 18px;
overflow: elide;
}
if root.update-available: Rectangle {
height: max(18px, version-link.preferred-height);
accessible-role: button;
accessible-label: UiStrings.release-notes + " " + root.latest-version;
accessible-action-default => { version-touch.clicked(); }
version-link := Text {
x: 0px;
y: 0px;
width: parent.width;
height: parent.height;
text: UiStrings.update-available + ": " + root.latest-version;
color: version-touch.has-hover ? #174ea6 : #334fd1;
font-size: ImGuiFontStyle.body;
vertical-alignment: center;
horizontal-alignment: left;
wrap: word-wrap;
}
version-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.about-open = false; root.update-open = true; }
}
}
if root.update-available: HorizontalLayout {
alignment: start;
padding-top: 4px;
CompactButton {
width: max(76px, release-notes-button-metrics.preferred-width + 20px);
text: UiStrings.view-release-notes;
accessible-role: button;
accessible-label: self.text;
accessible-action-default => { self.clicked(); }
clicked => { root.about-open = false; root.update-open = true; }
release-notes-button-metrics := Text { visible: false; text: UiStrings.view-release-notes; font-size: ImGuiFontStyle.body; }
}
}
if root.update-status-text != "": Text {
text: root.update-status-text;
color: root.update-check-status == UpdateCheckStatus.failed ? #b3261e : #61656c;
font-size: ImGuiFontStyle.body;
min-height: 18px;
wrap: word-wrap;
}
}
VerticalLayout {
vertical-stretch: 0;
spacing: 3px;
Rectangle { height: 1px; background: #e6e8eb; }
Text { height: 16px; text: UiStrings.copyright; color: #61656c; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
Text { height: 16px; text: UiStrings.license; color: #61656c; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
}
HorizontalLayout {
vertical-stretch: 0;
alignment: end;
spacing: 8px;
CompactButton {
width: max(76px, check-button-metrics.preferred-width + 20px);
text: UiStrings.check-for-updates;
enabled: root.update-check-status != UpdateCheckStatus.checking;
accessible-role: button;
accessible-label: self.text;
accessible-action-default => { if self.enabled { root.check-for-updates(); } }
clicked => { root.check-for-updates(); }
check-button-metrics := Text { visible: false; text: UiStrings.check-for-updates; font-size: ImGuiFontStyle.body; }
}
CompactButton { width: 48px; 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: 464px;
height: 400px;
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: 11px;
vertical-alignment: center;
}
website-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } }
}
}
}
VerticalLayout {
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;
}
}
}
}
}
}
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;
}
}
}