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 local-desktop: "Local Desktop"; in-out property remote-desktop: "Remote Desktop"; in-out property recent-connections: "Recent Connections"; in-out property local-id: "Local ID"; in-out property device-name: "Device Name"; in-out property remote-id: "Remote ID"; in-out property online: "Online"; in-out property offline: "Offline"; in-out property password: "Password"; in-out property refresh-password: "Refresh random password"; in-out property copied: "Local ID copied to clipboard"; in-out property connect: "Connect"; in-out property settings: "Settings"; in-out property about: "About"; in-out property ok: "OK"; in-out property cancel: "Cancel"; in-out property save: "Save"; in-out property new-password: "New password (6 characters)"; in-out property invalid-password: "Password must contain exactly 6 characters"; in-out property edit-alias: "Connection alias"; in-out property delete-connection: "Delete connection"; in-out property confirm-delete: "Delete this recent connection?"; in-out property language: "Language"; in-out property capture-method: "Capture method"; in-out property capture-method-auto: "Auto"; in-out property codec: "Video codec"; in-out property hardware-codec: "Hardware video codec"; in-out property turn-relay: "TURN relay"; in-out property force-relay: "Force relay"; in-out property self-hosted: "Self-hosted server"; in-out property autostart: "Start with system"; in-out property daemon: "Run in background"; in-out property save-remote-previews: "Save remote screen previews:"; in-out property unknown-platform: "Unknown platform"; in-out property privacy-on-connect: "Privacy screen on connect"; in-out property file-save-path: "File save path"; in-out property default-desktop: "Desktop"; in-out property server-host: "Server host"; in-out property server-port: "Signal port"; in-out property version: "Version"; in-out property copyright: "© 2026 by JUNKUN DI. All right reserved."; in-out property license: "Licensed under GNU GPL v3."; in-out property signal-connected: "Signal server connected"; in-out property signal-disconnected: "Signal server disconnected"; in-out property tls-error: "Signal server TLS certificate error"; in-out property signal-reconnect: "Reconnect"; in-out property signal-reconnecting: "Reconnecting..."; in-out property update-available: "A new version is available"; in-out property new-version: "New version"; in-out property check-for-updates: "Check for updates"; in-out property checking-for-updates: "Checking for updates…"; in-out property up-to-date: "You’re up to date"; in-out property update-check-failed: "Check failed. Please try again later."; in-out property release-notes: "Release notes"; in-out property view-release-notes: "Release notes"; in-out property later: "Later"; in-out property download: "Download"; in-out property input-password: "Input password"; in-out property reinput-password: "Incorrect password, please try again"; in-out property remember-password: "Remember password"; in-out property validate-password: "Validating password"; in-out property request-permissions: "Request permissions"; in-out property permission-required: "The application requires the following permissions:"; in-out property screen-recording-permission: "Screen recording permission"; in-out property accessibility-permission: "Accessibility permission"; in-out property wayland-notice-title: "Wayland Session Notice"; in-out property wayland-notice-subtitle; in-out property wayland-permission-title; in-out property wayland-notice-message; in-out property wayland-recommendation-title; in-out property wayland-recommendation-message; in-out property wayland-notice-acknowledge: "Got it"; in-out property service-setup-title: "Install CrossDesk Service"; in-out property service-setup-message: "Administrator permission is required to control this device while it is locked."; in-out property service-settings-label: "Lock Screen Service:"; in-out property install-service: "Install"; in-out property service-installed: "Installed"; in-out property do-not-remind: "Do not remind again"; in-out property notification: "Notification"; in-out property service-suppressed-message: "You will not be reminded again. You can install the service later in Settings."; in-out property codec-h264: "H.264"; in-out property codec-av1: "AV1"; in-out property self-hosted-settings: "Self-hosted server settings"; in-out property access-website: "Website: "; in-out property 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 prompt; in property text-color: #30343b; in property text-size: ImGuiFontStyle.base; in property 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 text; in property max-length: 0; in property text-size: ImGuiFontStyle.base; private property 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 gallery-mode: false; in-out property gallery-simplified: false; changed gallery-simplified => { root.reset-gallery-transient(); root.gallery-action("view-mode", ""); focus-sink.focus(); } in-out property gallery-network-open: false; in property gallery-resolution-index: 0; in property gallery-frame-rate-index: 0; in property gallery-bandwidth-index: 3; in-out property gallery-resolution-draft: 0; in-out property gallery-frame-rate-draft: 0; in-out property gallery-bandwidth-draft: 3; in property 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 gallery-fullscreen: false; in property 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 connection-input-error; in property gallery-controlling: false; in property gallery-remote-cursor-shape: 0; in property gallery-group-controlling: false; out property gallery-video-width: gallery.video-width; out property gallery-video-height: gallery.video-height; out property gallery-preview-width: gallery.preview-width; out property 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 gallery-group-index: 0; out property gallery-columns: gallery.columns; out property gallery-rows: gallery.rows; in-out property gallery-tile-width: 216; in-out property gallery-focus-rail-height: 70.875; out property gallery-resizing: gallery.resizing || gallery.focus-resizing; in property gallery-page: 0; in property gallery-pages: 1; in property gallery-count: 0; in property gallery-connected: 0; in property gallery-selected: 0; in property gallery-selected-audio-available: false; in property gallery-selected-audio-muted: false; in property gallery-hidden-selected: 0; in property gallery-notice; in property <[GalleryActionDevice]> gallery-selected-devices; in property gallery-focused; in-out property gallery-batch: false; in-out property gallery-adding: false; in-out property gallery-importing: false; in property <[GalleryRecentDevice]> gallery-recent-devices; in property gallery-recent-selected: 0; in property gallery-recent-available: 0; in property 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 device-group-open: false; private property device-group-id; private property device-group-name; in property <[DeviceGroupOption]> device-group-options; in property device-group-error; in-out property 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 custom-titlebar: false; in property wayland-titlebar: false; in property window-active: true; private property titlebar-height: root.custom-titlebar && !root.gallery-fullscreen ? (root.wayland-titlebar ? 32px : 31px) : 0px; out property minimum-window-width: 640px; out property minimum-window-height: (root.gallery-mode ? 448px : 472px) + root.titlebar-height; private property wayland-corner-radius: 12px; in property local-id: ""; in property local-password: ""; in-out property remote-id-input: ""; in property password-visible: true; in property password-change-pending: false; in property signal-connected: false; in property signal-reconnecting: false; in property signal-tls-error: false; in property <[RecentConnection]> recent-connections; in property <[string]> recent-groups: [GalleryStrings.tr("全部设备", "All devices", "Все устройства"), GalleryStrings.tr("未分组", "Ungrouped", "Без группы")]; in-out property 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 announcement-unread; private property announcement-unread-label: root.announcement-unread > 99 ? "99+" : "" + root.announcement-unread; in property announcement-total; in property announcement-loading; in property announcement-failed; in property announcement-read-save-failed; in property announcement-delete-failed; in property announcement-loaded; in-out property messages-open: false; in property announcement-selected-id; in property announcement-selected-title; in property announcement-selected-body; in property announcement-selected-date; in property 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 update-available: false; in property update-check-status: idle; private property 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 current-version: ""; in property latest-version: ""; in property release-name: ""; in property <[ReleaseNoteBlock]> release-note-blocks; in property release-date: ""; in-out property offline-warning: ""; in property connection-dialog-open: false; in property connection-status-text: ""; in property connection-pending: false; in property connection-password-required: false; in property connection-validating: false; in-out property connection-password: ""; in-out property connection-remember-password: false; in property permission-dialog-open: false; in property screen-recording-granted: true; in property accessibility-granted: true; in-out property wayland-notice-open: false; in property portable-service-settings-visible: false; in property portable-service-dialog-open: false; in property portable-service-suppressed-notice-open: false; in property portable-service-installed: false; in property portable-service-installing: false; in property portable-service-succeeded: false; in property portable-service-status: ""; in-out property portable-service-do-not-remind: false; in-out property language-index: 0; in property capture-method-visible: false; in-out property capture-method-index: 0; in-out property codec-index: 0; in-out property hardware-codec-enabled: true; in-out property turn-enabled: true; in-out property force-relay-enabled: false; changed turn-enabled => { if !root.turn-enabled { root.force-relay-enabled = false; } } in-out property self-hosted-enabled: false; in-out property autostart-enabled: false; in-out property daemon-enabled: false; in-out property save-remote-previews: true; // Show the preference on every platform; only adapted hosts can change it. in property privacy-setting-available: false; in-out property privacy-on-connect-enabled: true; in-out property file-save-path: ""; in-out property server-host: ""; in-out property server-port: ""; in-out property self-host-settings-error: ""; in property 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 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 menu-open: false; in-out property settings-open: false; in-out property settings-tab: 0; private property settings-scroll-y: 0px; in-out property self-host-settings-open: false; changed self-host-settings-open => { if !root.self-host-settings-open { root.self-host-settings-error = ""; } } in-out property 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 update-open: root.update-available; in-out property reset-password-open: false; in-out property reset-password-invalid: false; in-out property new-password-input: ""; in-out property alias-open: false; private property alias-remote-id: ""; in-out property alias-input: ""; in-out property delete-open: false; private property delete-remote-id: ""; private property delete-from-gallery: false; private property copy-toast: false; private property compact-language: root.language-index == 0; private property recent-tooltip-visible: false; private property recent-tooltip-name: ""; private property recent-tooltip-host: ""; private property recent-tooltip-id: ""; private property recent-tooltip-groups: ""; private property recent-tooltip-online: false; private property recent-tooltip-pointer-x: 0px; private property 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 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 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 available: connection.online || connection.connected; private property content-width: self.width; private property toolbar-width: DeviceCardStyle.toolbar-width; private property 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 group-names: connection.group-names; private property 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 device-name: root.gallery-mode ? gallery.hovered-device.name : root.recent-tooltip-name; private property host-name: root.gallery-mode ? gallery.hovered-device.host-name : root.recent-tooltip-host; private property device-id: root.gallery-mode ? gallery.hovered-device.id : root.recent-tooltip-id; private property group-names: root.gallery-mode ? gallery.hovered-device.group-names : root.recent-tooltip-groups; private property online: root.gallery-mode ? gallery.hovered-device.online : root.recent-tooltip-online; private property presence-known: !root.gallery-mode || gallery.hovered-device.presence-known; private property pointer-x: root.gallery-mode ? gallery.x + gallery.device-tooltip-x : root.recent-tooltip-pointer-x; private property 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 content-inset: 26px; private property 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; } } }