import { SurfaceSeparator, FlatSurface, UiSurface } from "ui_surface.slint"; import { TrustedDevicesDialog, TrustedDeviceEntry, TrustStrings } from "trusted_devices.slint"; import { LanStrings } from "lan_strings.slint"; import { AboutPanel, AboutPage, AboutComponent, AboutStrings, AboutNavigationRow, AboutNavigationGroup, AboutRowSeparator } from "about_panel.slint"; import { UiRadius } from "ui_radius.slint"; import { WorkbenchStyle, WorkbenchButton, WorkbenchCard, WorkbenchNotice } from "workbench_controls.slint"; import { GalleryStrings } from "device_strings.slint"; import { DeviceCardStyle } from "device_card_actions.slint"; import { GalleryMenuButton } from "gallery_menu_button.slint"; import { DeviceGroupDialog, DeviceGroupOption } from "device_group_dialog.slint"; import { GalleryView, GalleryTile, GalleryRecentDevice, GalleryActionDevice } from "gallery_view.slint"; import { GalleryNetworkDialog, GalleryNetworkDevice } from "gallery_network_dialog.slint"; import { SettingsDialog, SettingsButton, SettingsSelect, SettingsGroup, SettingsRow, SettingsToggleRow } from "settings_controls.slint"; import { GallerySettings } from "gallery_settings.slint"; import { GroupFilter } from "group_filter.slint"; import { ScrollView } from "std-widgets.slint"; import { RoundedLineEdit } from "rounded_line_edit.slint"; import { AnnouncementItem, MessagePanel, MessageStrings } from "message_panel.slint"; import { FontAwesomeIcons, IconButton, CompactComboBox, ImGuiCheckBox, CompactButton, PrimaryButton, SecondaryButton, ToggleSwitch, SectionTitle, DialogSurface, ImGuiFontStyle, ControlStyle } from "common.slint"; import "../assets/fonts/message_spacing.ttf"; export struct RecentConnection { remote-id: string, display-name: string, host-name: string, group-names: string, online: bool, lan: bool, endpoint: string, connected: bool, thumbnail: image, has-thumbnail: bool, platform: string, } export struct ReleaseNoteBlock { content: styled-text, section-gap: bool, } export enum UpdateCheckStatus { idle, checking, up-to-date, available, failed } export enum AutostartStatus { checking, enabled, disabled, unsupported, failed } enum CopyFeedback { idle, success, failed } enum SponsorPlatform { afdian, kofi, github } export global UiStrings { in-out property window-minimize: "Minimize"; in-out property window-close: "Close window"; 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 sponsorship-title: "Sponsorship"; in-out property sponsorship-message; in-out property sponsorship-dismiss-hint; in-out property sponsor-via-afdian: "AFDIAN"; 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: "Use exactly 6 letters (A–Z, a–z) or digits (0–9)."; 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 codec: "Video codec"; in-out property pause-video-when-minimized: "Pause video when minimized"; in-out property pause-video-when-minimized-hint: "Pause video reception when the control window is minimized to reduce local decoding resource usage."; 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-local-unlock: "Allow privacy screen unlock while controlled"; in-out property privacy-on-connect: "Privacy screen on connect"; in-out property lock-on-disconnect: "Lock screen on disconnect"; 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 rights 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: "Server certificate invalid or expired. Retrying automatically."; 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 install-update: "Install update"; in-out property retry-update: "Retry update"; in-out property install-update-confirm; 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 system-permissions: "System permissions"; in-out property permission-migration-running: "Migrating permissions…"; in-out property permission-migration-failed; in-out property retry-permission-migration: "Retry migration"; in-out property permission-later: "Later"; 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 control service"; in-out property install-service: "Install"; in-out property service-installed: "Installed"; in-out property repair-service: "Repair"; in-out property service-repair-title: "Repair CrossDesk Service"; in-out property service-repair-message; 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: "; } // Keep sponsor labels, order, and sizing consistent across dialogs. component SponsorshipSection inherits VerticalLayout { in property show-title: true; callback open-afdian; callback open-kofi; callback open-github; vertical-stretch: 0; spacing: 10px; if root.show-title: Text { text: UiStrings.sponsorship-title; color: WorkbenchStyle.text; font-size: 12px; font-weight: 600; wrap: word-wrap; } HorizontalLayout { spacing: 8px; for platform in (GalleryStrings.language == 0 ? [SponsorPlatform.afdian, SponsorPlatform.kofi, SponsorPlatform.github] : [SponsorPlatform.kofi, SponsorPlatform.github, SponsorPlatform.afdian]): Rectangle { // Size to the label, icon, and padding, then share extra row space. min-width: sponsor-label-metrics.preferred-width + 56px; preferred-width: self.min-width; horizontal-stretch: 1; height: 32px; SettingsButton { width: parent.width; height: parent.height; icon: platform == SponsorPlatform.afdian ? @image-url("icons/sponsors/afdian.png") : platform == SponsorPlatform.kofi ? @image-url("icons/sponsors/kofi.png") : @image-url("icons/sponsors/github.svg"); text: platform == SponsorPlatform.afdian ? UiStrings.sponsor-via-afdian : platform == SponsorPlatform.kofi ? "Ko-fi" : "GitHub Sponsors"; sponsor-label-metrics := Text { visible: false; text: parent.text; font-size: 12px; } clicked => { if platform == SponsorPlatform.afdian { root.open-afdian(); } else if platform == SponsorPlatform.kofi { root.open-kofi(); } else { root.open-github(); } } } } } } // Prompt copy in modal dialogs sits slightly low in the legacy ImGui layout. // Offset only the rendered text so input fields and action buttons keep their // established geometry. component DialogPromptText inherits Rectangle { in property 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; } } // Service installation, repair and reminder notices share the settings style. // Keep actions visible while long translations can scroll within the window. component ServicePromptDialog inherits DialogSurface { in property title; in property message; in property status; in property primary-text; in property secondary-text; in property opt-out-label; in property busy: false; in property succeeded: false; in property show-opt-out: false; in-out property opt-out: false; in property available-height; callback primary-action; callback secondary-action; height: min(root.available-height, content.preferred-height + 113px); background: #f8fafc; outline-color: UiSurface.edge; TouchArea { scroll-event(event) => { accept } } ScrollView { x: 24px; y: 24px; width: parent.width - 40px; height: parent.height - 113px; horizontal-scrollbar-policy: always-off; vertical-scrollbar-policy: as-needed; viewport-width: self.visible-width; viewport-height: content.preferred-height; content := VerticalLayout { // Reserve the scrollbar gutter independently of its visibility. x: 0px; y: 0px; width: root.width - 64px; height: self.preferred-height; padding: 0px; spacing: 16px; Text { text: root.title; color: WorkbenchStyle.text; font-size: 20px; font-weight: 600; wrap: word-wrap; } Text { text: root.message; color: WorkbenchStyle.text; font-size: 14px; wrap: word-wrap; font-family: "CrossDesk Message Spacing"; } if root.status != "": Text { text: root.status; font-size: 12px; wrap: word-wrap; font-family: "CrossDesk Message Spacing"; color: root.succeeded ? WorkbenchStyle.success : root.busy ? WorkbenchStyle.muted : UiSurface.error; } if root.show-opt-out: HorizontalLayout { spacing: 12px; Text { text: root.opt-out-label; color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap; vertical-alignment: center; horizontal-stretch: 1; } ToggleSwitch { checked <=> root.opt-out; enabled: !root.busy; accessible-role: switch; accessible-label: root.opt-out-label; accessible-checked: root.opt-out; accessible-enabled: !root.busy; accessible-action-default => { if !root.busy { root.opt-out = !root.opt-out; } } } } } } SurfaceSeparator { x: 24px; y: parent.height - 73px; width: parent.width - 48px; outline-color: UiSurface.separator; } HorizontalLayout { x: 24px; y: parent.height - 56px; width: parent.width - 48px; height: 32px; alignment: end; spacing: 8px; if root.secondary-text != "": SettingsButton { width: max(80px, self.label-width); text: root.secondary-text; enabled: !root.busy; clicked => { root.secondary-action(); } } SettingsButton { width: max(80px, self.label-width); primary: true; text: root.primary-text; enabled: !root.busy; clicked => { root.primary-action(); } } } } // Use the same plain-text field for connection and password changes. component PasswordInput inherits FlatSurface { in-out property text; in property max-length: 0; in property text-size: ImGuiFontStyle.base; in property error: false; private property previous-text; callback accepted; width: 70px; height: 23px; background: editor.has-focus ? ControlStyle.input-focused : ControlStyle.input-surface; outline-color: root.error ? UiSurface.error : editor.has-focus ? UiSurface.focus : UiSurface.edge; outline-focused: editor.has-focus || root.error; border-radius: UiRadius.control; clip: true; forward-focus: editor; init => { root.previous-text = root.text; } editor := TextInput { x: 12px; y: 1px; width: parent.width - 24px; 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.texts[54] : "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 property ip-direct-enabled: true; changed ip-direct-enabled => { if !root.ip-direct-enabled { root.lan-port-dirty = false; root.lan-port-editing = false; root.lan-apply-pending = false; root.lan-local-port = root.lan-listener-port; root.lan-endpoint-error = ""; root.lan-access-warning = ""; root.lan-access-warning-endpoint = ""; root.lan-listener-error = ""; root.lan-open = false; root.lan-connecting = false; root.lan-listening = false; root.lan-password-pending = false; root.lan-retry-after = 0; root.lan-access-password = ""; root.lan-new-password = ""; root.lan-message = ""; lan-ip-picker.close-menu(); if root.lan-mode { root.reset-password-open = false; root.set-connection-mode(false); } } } in-out property lan-open; changed lan-open => { if !root.lan-open { root.lan-access-password = ""; } if root.lan-open { root.gallery-action("release", ""); } } in-out property lan-mode; callback connection-mode-changed(bool); changed lan-mode => { root.recent-tooltip-visible = false; recent-scroll.viewport-x = 0px; if !root.lan-mode { root.lan-port-editing = false; if root.lan-port-dirty && root.lan-endpoint-error == "" { root.lan-apply-pending = true; } } } private property id-connection-draft; private property ip-connection-draft; private property lan-password-visible: true; private property active-password-visible: root.lan-mode ? root.lan-password-visible : root.password-visible; private property active-password: root.lan-mode ? root.lan-new-password : root.local-password; function set-connection-mode(lan: bool) { recent-group-filter.close-menu(); if root.lan-mode { root.ip-connection-draft = root.remote-id-input; } else { root.id-connection-draft = root.remote-id-input; } root.lan-mode = lan; root.remote-id-input = root.lan-mode ? root.ip-connection-draft : root.id-connection-draft; root.connection-input-error = ""; root.copy-feedback = CopyFeedback.idle; root.menu-open = false; focus-sink.focus(); } function select-connection-mode(lan: bool) { if !root.ip-direct-enabled || root.connection-pending || root.lan-connecting || root.lan-mode == lan { return; } root.set-connection-mode(lan); root.connection-mode-changed(lan); } function submit-connection() { if root.connection-pending || root.lan-connecting { return; } if root.lan-mode { root.request-lan-connection(root.remote-id-input); } else { root.connect-requested(root.remote-id-input); } } public function request-lan-connection(address: string) { if !root.ip-direct-enabled || root.connection-pending || root.lan-connecting || root.lan-open { return; } root.lan-access-password = ""; root.lan-remember-password = false; let error = root.lan-prepare-connect(address); root.connection-input-error = error == "Invalid LAN endpoint" ? GalleryStrings.texts[55] : LanStrings.error-message(error); if error == "" { root.lan-open = true; if root.lan-remember-password && root.lan-access-password != "" && root.lan-retry-after == 0 { root.submit-connection-password(); } } } in-out property lan-listening; in-out property lan-listen-all: true; in-out property lan-listener-ip; in-out property lan-listener-ipv6; in-out property lan-listener-port: "52888"; out property lan-listener-address: root.lan-listener-ip == "" ? "" : (root.lan-listener-ipv6 ? "[" + root.lan-listener-ip + "]" : root.lan-listener-ip) + ":" + root.lan-listener-port; in-out property <[string]> lan-interfaces; in-out property <[string]> lan-interface-labels: root.lan-interfaces; in-out property lan-interface-index: 0; in-out property lan-interface-draft-index: 0; in-out property lan-local-port: "52888"; in-out property lan-port-editing; in-out property lan-port-dirty; in-out property lan-endpoint-error; in-out property lan-access-warning; in-out property lan-access-warning-endpoint; in-out property lan-listener-error; function dismiss-lan-access-warning() { root.lan-access-warning = ""; root.lan-access-warning-endpoint = ""; // Keep the failed draft ready to edit and validate it on every new blur. if root.lan-mode && root.lan-port-dirty && root.sponsorship-unobstructed { lan-port-editor.focus(); } else { focus-sink.focus(); } } private property lan-apply-pending; callback lan-configure(string, string) -> bool; callback lan-select-address(string) -> bool; function apply-lan-endpoint() -> bool { root.lan-apply-pending = false; if !root.ip-direct-enabled || root.lan-password-pending { return false; } if root.lan-configure(root.lan-listener-ip, root.lan-local-port) { root.lan-port-dirty = false; return true; } root.lan-access-warning-endpoint = (root.lan-listener-ipv6 ? "[" + root.lan-listener-ip + "]" : root.lan-listener-ip) + (root.lan-endpoint-error == "Invalid LAN port" ? "" : ":" + root.lan-local-port); root.lan-access-warning = root.lan-endpoint-error; return false; } Timer { interval: 1ms; running: root.lan-apply-pending; triggered => { root.lan-apply-pending = false; if root.lan-port-dirty { root.apply-lan-endpoint(); } } } in-out property lan-remote-address; in-out property lan-remote-port: "52888"; in-out property lan-password-pending; in-out property lan-connecting; in-out property lan-retry-after; in-out property lan-access-password; in-out property lan-remember-password; in-out property lan-new-password; in-out property lan-message; callback lan-reset-password; callback lan-copy-text(string) -> bool; callback lan-cancel-connect; callback lan-prepare-connect(string) -> string; callback lan-connect(string, string, string, bool); private property prompt-password-required: root.lan-open || root.connection-password-required; private property prompt-validating: root.lan-open ? root.lan-connecting : root.connection-validating; private property prompt-pending: root.lan-open ? root.lan-connecting : root.connection-pending; private property prompt-status-text: !root.lan-open ? root.connection-status-text : root.lan-connecting ? GalleryStrings.texts[56] : root.lan-retry-after > 0 ? GalleryStrings.retry-after(root.lan-retry-after) : root.lan-message == "InvalidCredentials" ? UiStrings.reinput-password : root.lan-message == "" || root.lan-message == "AuthenticationRateLimited" ? UiStrings.input-password : LanStrings.error-message(root.lan-message); function submit-connection-password() { if root.lan-open { if root.lan-connecting || root.lan-retry-after > 0 || root.lan-access-password.character-count != 6 { return; } root.lan-connect(root.lan-remote-address, root.lan-remote-port, root.lan-access-password, root.lan-remember-password); root.lan-access-password = ""; } else { root.connection-submit-password(root.connection-password, root.connection-remember-password); } } function cancel-connection-prompt() { if root.lan-open { root.lan-cancel-connect(); root.lan-access-password = ""; root.lan-open = false; focus-sink.focus(); } else { root.connection-cancel(); } } 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; out property recent-preview-width: max(1px, (recent-scroll.width - 24px) / 3); out property recent-preview-height: 80px; in property <[string]> recent-groups: [GalleryStrings.texts[57], GalleryStrings.texts[31]]; 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-revision; 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 read-all-announcements; callback load-more-announcements; in property update-available: false; in property update-install-available: false; in property update-install-busy: false; in property update-install-failed: false; changed update-install-available => { if !root.update-install-available { root.update-confirm-open = false; } } changed update-install-busy => { if root.update-install-busy { root.update-confirm-open = false; } } in property update-install-status: ""; private property update-action-text: !root.update-install-available ? UiStrings.download : root.update-install-failed && !root.update-install-busy ? UiStrings.retry-update : UiStrings.install-update; 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 mac-permission-settings-visible: false; in property permission-migration-running: false; in property permission-migration-failed: 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-needs-repair: 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: 10; in property <[string]> language-options; in-out property codec-index: 0; in-out property ip-direct-enabled-draft: true; in-out property lan-listen-all-draft: true; in property settings-save-error; in-out property force-relay-enabled: false; in-out property self-hosted-enabled: false; in-out property autostart-enabled: false; in property autostart-available: true; in property service-autostart-platform: false; in property service-autostart-hint; in property autostart-system-managed: false; in property autostart-status: AutostartStatus.checking; out property autostart-status-text: root.autostart-status == AutostartStatus.checking ? GalleryStrings.texts[58] : root.autostart-status == AutostartStatus.enabled ? GalleryStrings.texts[59] : root.autostart-status == AutostartStatus.disabled ? GalleryStrings.texts[60] : root.autostart-status == AutostartStatus.unsupported ? GalleryStrings.texts[61] : GalleryStrings.texts[62]; in-out property autostart-guide-open: false; changed autostart-guide-open => { if root.autostart-guide-open { root.dialog-focus-pending = true; } } in-out property daemon-enabled: false; in-out property save-remote-previews: true; in-out property pause-video-when-minimized: 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 privacy-local-unlock-allowed: true; in property lock-on-disconnect-available: false; in-out property lock-on-disconnect-enabled: true; in-out property trusted-devices-only: false; in-out property trusted-devices-open: false; in property <[TrustedDeviceEntry]> trusted-devices; in property <[TrustedDeviceEntry]> trust-candidates; in property device-fingerprint; in property trusted-devices-error; in property trusted-devices-notice; in property trust-copy-error; in property trust-copy-notice; in-out property trust-pairing-input; callback trust-action(string, string, string); 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; callback copy-local-id() -> bool; callback toggle-password-visibility; callback refresh-password; callback reset-password(string) -> bool; callback format-remote-id(string) -> string; callback connect-requested(string); callback recent-connect(string); callback recent-disconnect(string); callback recent-edit-alias(string, string); callback recent-delete(string); callback save-settings() -> bool; callback cancel-settings; callback refresh-autostart-status; callback save-self-hosted-settings() -> bool; callback cancel-self-hosted-settings; callback browse-save-path; callback open-download; callback open-manual-download; callback confirm-update; callback cancel-update; callback open-website; callback open-afdian-sponsor; callback open-github-sponsor; callback open-kofi-sponsor; callback dismiss-sponsorship; 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 open-permissions; callback dismiss-permissions; callback retry-permission-migration; callback install-portable-service; callback open-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; private property pause-video-hint-visible: false; private property pause-video-hint-x: 0px; private property pause-video-hint-y: 0px; changed settings-tab => { root.pause-video-hint-visible = false; } changed settings-scroll-y => { root.pause-video-hint-visible = false; } 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; in-out property about-page: overview; private property about-scroll-y: 0px; in-out property about-component-index: 0; in property <[AboutComponent]> about-components; in property about-openfec-index: -1; in property about-revision; in property about-source-url; in property about-build-url; callback open-about-link(string); function about-back() { if root.about-page == AboutPage.document { root.about-page = AboutPage.component; } else if root.about-page == AboutPage.component && root.about-component-index != 0 { root.about-page = AboutPage.components; } else { root.about-page = AboutPage.overview; } } in-out property sponsorship-open: false; // Automatic prompts must not interrupt another dialog or menu. private property sponsorship-unobstructed: !root.gallery-mode && !root.menu-open && !root.settings-open && !root.self-host-settings-open && !root.about-open && !(root.update-open && root.update-available) && !root.messages-open && !root.update-confirm-open && !root.reset-password-open && !root.alias-open && !root.delete-open && !root.disconnect-open && !root.device-group-open && !root.connection-dialog-open && !root.lan-open && !root.permission-dialog-open && !root.wayland-notice-open && !root.portable-service-dialog-open && !root.portable-service-suppressed-notice-open && root.offline-warning == "" && root.lan-access-warning == ""; out property can-show-sponsorship: !root.sponsorship-open && root.sponsorship-unobstructed; // 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; changed update-open => { if !root.update-open { root.update-confirm-open = false; } } in-out property update-confirm-open: false; in-out property reset-password-open: false; changed reset-password-open => { root.new-password-input = ""; root.reset-password-invalid = 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; in-out property disconnect-open: false; private property disconnect-remote-id: ""; private property disconnect-device-name: ""; function dismiss-disconnect() { root.disconnect-open = false; root.disconnect-remote-id = ""; root.disconnect-device-name = ""; focus-sink.focus(); } private property copy-feedback: idle; private property compact-language: GalleryStrings.language == 0 || GalleryStrings.language == 3 || GalleryStrings.language == 4 || GalleryStrings.language == 5; private property recent-tooltip-visible: false; private property recent-tooltip-index: -1; private property recent-tooltip-card-x: 0px; 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; private property dialog-focus-pending: false; // A changed handler can run lazily while winit's accessibility adapter // borrows its tree. Switching focus there reenters that adapter and panics. // Coalesce requests and move focus on the next event-loop turn instead. Timer { interval: 1ms; running: root.dialog-focus-pending; triggered => { root.dialog-focus-pending = false; if root.settings-open || root.autostart-guide-open || root.wayland-notice-open || root.messages-open || root.about-open || (root.sponsorship-open && root.sponsorship-unobstructed) { focus-sink.focus(); } } } changed menu-open => { if root.menu-open { root.copy-feedback = CopyFeedback.idle; root.gallery-action("release", ""); } } changed connection-dialog-open => { if root.connection-dialog-open { root.gallery-action("release", ""); } } changed permission-dialog-open => { if root.permission-dialog-open { root.gallery-action("release", ""); } } changed settings-open => { root.pause-video-hint-visible = false; if root.settings-open { root.settings-tab = 0; root.settings-scroll-y = 0px; root.menu-open = false; root.gallery-network-open = false; root.gallery-resolution-draft = root.gallery-resolution-index; root.gallery-frame-rate-draft = root.gallery-frame-rate-index; root.gallery-bandwidth-draft = root.gallery-bandwidth-index; root.gallery-action("settings-open", ""); if root.autostart-system-managed { root.refresh-autostart-status(); } root.dialog-focus-pending = true; } else { root.autostart-guide-open = false; } } changed wayland-notice-open => { if root.wayland-notice-open { root.dialog-focus-pending = true; } } changed messages-open => { if root.messages-open { root.dialog-focus-pending = true; } } changed about-open => { if root.about-open { root.dialog-focus-pending = true; } else { root.about-page = AboutPage.overview; root.about-component-index = 0; root.about-scroll-y = 0px; } } changed about-page => { // Restore dialog shortcuts after leaving the selectable license text. if root.about-open { root.dialog-focus-pending = true; } } changed sponsorship-open => { if root.sponsorship-open { root.dialog-focus-pending = true; } } 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 => { if self.pressed { root.copy-feedback = CopyFeedback.idle; } root.main-title-drag(phase, self.mouse-x, self.mouse-y); } moved => { if (self.pressed) { root.main-title-drag(2, self.mouse-x, self.mouse-y); } } } wayland-minimize-button := Rectangle { x: parent.width - 74px; y: 4px; width: 24px; height: 24px; border-radius: 12px; background: wayland-minimize-touch.pressed ? #c0bfbc : wayland-minimize-touch.has-hover ? #deddda : root.window-active ? #e6e6e6 : transparent; accessible-role: button; accessible-label: UiStrings.window-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: UiStrings.window-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(); } } } SurfaceSeparator { y: parent.height - 1px; height: 1px; outline-color: UiSurface.separator; } } 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 => { if self.pressed { root.copy-feedback = CopyFeedback.idle; } root.main-title-drag(phase, self.mouse-x, self.mouse-y); } moved => { if (self.pressed) { root.main-title-drag(2, self.mouse-x, self.mouse-y); } } } minimize-button := Rectangle { x: parent.width - 92px; width: 46px; height: parent.height; background: minimize-touch.pressed ? #c7c7c7 : minimize-touch.has-hover ? #e5e5e5 : transparent; accessible-role: button; accessible-label: UiStrings.window-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: UiStrings.window-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(); } } } SurfaceSeparator { y: parent.height - 1px; height: 1px; outline-color: UiSurface.separator; } } content-layer := Rectangle { x: 0px; y: root.titlebar-height; width: root.width; height: root.height - self.y; clip: true; Rectangle { width: parent.width; height: parent.height; background: root.gallery-mode ? white : WorkbenchStyle.canvas; border-width: 0px; border-radius: 0px; focus-sink := FocusScope { x: 0px; y: 0px; width: 0px; height: 0px; key-pressed(event) => { if root.autostart-guide-open { if event.text == Key.Return || event.text == Key.Escape { root.autostart-guide-open = false; } return accept; } if root.wayland-notice-open { if event.text == Key.Return || event.text == Key.Escape { root.wayland-notice-open = false; } return accept; } if root.about-open && !root.update-open { if event.text == Key.Escape { if root.about-page != AboutPage.overview { root.about-back(); } else { root.about-open = false; } } return accept; } if root.sponsorship-open && root.sponsorship-unobstructed { if event.text == Key.Escape { root.dismiss-sponsorship(); } 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 && !root.trusted-devices-open { if event.text == Key.Escape { root.cancel-settings(); root.settings-open = false; } return accept; } return reject; } } TouchArea { pointer-event(event) => { if event.kind == PointerEventKind.down { root.copy-feedback = CopyFeedback.idle; } } clicked => { focus-sink.focus(); } } // Slint owns the client area below the native macOS title bar. Draw // the boundary here because AppKit's titlebar separator is not shown // reliably when winit creates a window without an NSToolbar. SurfaceSeparator { x: 0px; y: 0px; width: parent.width; height: 1px; outline-color: UiSurface.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. connection-mode-switch := WorkbenchButton { visible: !root.gallery-mode && root.ip-direct-enabled; x: 20px; y: 6px; width: 136px; height: 28px; background: WorkbenchStyle.field-surface; enabled: !root.connection-pending && !root.lan-connecting; tooltip: root.lan-mode ? GalleryStrings.texts[63] : GalleryStrings.texts[64]; accessible-role: switch; accessible-label: GalleryStrings.texts[65]; accessible-checked: root.lan-mode; z: 8; clicked => { root.select-connection-mode(!root.lan-mode); connection-mode-switch.focus(); } Rectangle { x: root.lan-mode ? parent.width / 2 + 1px : 2px; y: 2px; width: (parent.width - 6px) / 2; height: parent.height - 4px; border-radius: UiRadius.control; background: WorkbenchStyle.accent; animate x { duration: 180ms; easing: ease-in-out; } } for lan in [false, true]: Text { x: lan ? parent.width / 2 + 1px : 2px; y: 2px; width: (parent.width - 6px) / 2; height: parent.height - 4px; text: lan ? GalleryStrings.texts[66] : GalleryStrings.texts[67]; font-size: 12px; font-weight: 500; color: root.lan-mode == lan ? white : WorkbenchStyle.muted; horizontal-alignment: center; vertical-alignment: center; accessible-role: none; animate color { duration: 180ms; } } } open-gallery-button := WorkbenchButton { visible: !root.gallery-mode; x: root.ip-direct-enabled ? connection-mode-switch.x + connection-mode-switch.width + 8px : 20px; y: connection-mode-switch.y; width: 80px; height: 28px; normal-background: white; text: GalleryStrings.texts[54]; 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; normal-background: white; icon: FontAwesomeIcons.bars; tooltip: GalleryStrings.texts[68]; 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: FlatSurface { x: menu-button.x + menu-button.width - self.width + 4px; y: menu-button.y - 1px; width: 16px; height: 16px; border-radius: 8px; outline-color: UiSurface.edge; 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: FlatSurface { 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; outline-color: UiSurface.edge; 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: root.lan-mode ? GalleryStrings.texts[69] : GalleryStrings.texts[70]; font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; } local-address-label := Text { x: 16px; y: 68px; height: 18px; text: root.lan-mode ? GalleryStrings.texts[71] : UiStrings.local-id; font-size: 12px; color: WorkbenchStyle.muted; vertical-alignment: top; } lan-switch-ip-metrics := Text { visible: false; accessible-role: none; text: GalleryStrings.texts[72]; font-size: 12px; font-weight: 500; } lan-edit-port-metrics := Text { visible: false; accessible-role: none; text: GalleryStrings.texts[73]; font-size: 12px; font-weight: 500; } lan-switch-ip-action := WorkbenchButton { visible: root.lan-mode; x: local-address-label.x + local-address-label.preferred-width + 4px; y: local-address-label.y - 5px; width: lan-switch-ip-metrics.preferred-width + 20px; height: 22px; quiet: true; text: lan-switch-ip-metrics.text; text-size: 12px; text-color: WorkbenchStyle.accent; enabled: root.lan-mode && !root.lan-password-pending && root.lan-interfaces.length > 0; clicked => { if root.lan-port-dirty && !root.apply-lan-endpoint() { return; } lan-ip-picker.open-menu(); } } WorkbenchButton { visible: root.lan-mode; x: lan-switch-ip-action.x + lan-switch-ip-action.width + 2px; y: lan-switch-ip-action.y; width: lan-edit-port-metrics.preferred-width + 20px; height: 22px; quiet: true; text: lan-edit-port-metrics.text; text-size: 12px; text-color: WorkbenchStyle.accent; enabled: root.lan-mode && !root.lan-password-pending && (root.lan-listen-all || root.lan-interfaces.length > 0); clicked => { // Moving from the editor to this action is still editing. root.lan-apply-pending = false; lan-port-editor.focus(); lan-port-editor.select-all(); } } local-address-field := FlatSurface { x: 16px; y: 90px; width: parent.width - 32px; height: 40px; // Reserve five port digits. Long IPv6 addresses elide at a // readable size; the picker and Copy retain the full value. private property lan-font-scale: max(0.75, min(1, (self.width - 80px) / (lan-ip-metrics.preferred-width + lan-port-metrics.preferred-width))); lan-ip-metrics := Text { visible: false; accessible-role: none; text: root.lan-interface-labels[root.lan-interface-draft-index]; font-size: 18px; font-weight: 600; } lan-port-metrics := Text { visible: false; accessible-role: none; text: "65535"; font-size: 18px; font-weight: 600; } // Raise the whole field: a child's z cannot escape its // parent's stacking order above the following password row. z: 1; background: ControlStyle.input-surface; border-radius: UiRadius.control; outline-color: !root.lan-mode ? UiSurface.edge : root.lan-endpoint-error != "" ? UiSurface.error : lan-ip-picker.has-focus || root.lan-port-editing ? UiSurface.focus : UiSurface.edge; animate outline-color { duration: 120ms; } if !root.lan-mode: Text { x: 12px; width: parent.width - 64px; height: parent.height; text: root.local-id; font-size: 24px; font-weight: 600; letter-spacing: 1px; overflow: elide; color: WorkbenchStyle.text; vertical-alignment: center; } SurfaceSeparator { x: parent.width - 48px; y: 10px; width: 1px; height: parent.height - 20px; vertical: true; outline-color: UiSurface.separator; } lan-ip-picker := GroupFilter { visible: root.lan-mode; x: 4px; y: 2px; width: min(lan-ip-metrics.preferred-width * local-address-field.lan-font-scale + 16px, parent.width - 64px - lan-port-metrics.preferred-width * local-address-field.lan-font-scale); height: parent.height - 4px; embedded: true; text-size: 18px * local-address-field.lan-font-scale; text-weight: 600; text-color: WorkbenchStyle.text; show-indicator: false; popup-min-width: max(160px, min(root.width - 80px, lan-ip-metrics.preferred-width + 40px)); model: root.lan-interface-labels; system-count: 0; current-index <=> root.lan-interface-draft-index; enabled: root.lan-mode && !root.lan-password-pending && root.lan-interfaces.length > 0; normal-outline-color: UiSurface.edge; hover-outline-color: UiSurface.hover; focus-outline-color: WorkbenchStyle.accent; accessible-label: GalleryStrings.texts[74]; selected(index) => { root.copy-feedback = CopyFeedback.idle; // Save the draft for its original IP before changing // the display. Selecting an IP never rebinds sockets. if root.lan-port-dirty && !root.apply-lan-endpoint() { root.lan-interface-draft-index = root.lan-interface-index; return; } if root.lan-select-address(root.lan-interfaces[index]) { root.lan-interface-index = index; } else { root.lan-access-warning-endpoint = root.lan-interfaces[index]; root.lan-access-warning = root.lan-endpoint-error; root.lan-interface-draft-index = root.lan-interface-index; } } } if root.lan-mode: Text { x: lan-ip-picker.x + lan-ip-picker.width - 4px; width: 6px; height: parent.height; text: ":"; font-size: 18px * local-address-field.lan-font-scale; font-weight: 600; color: WorkbenchStyle.text; horizontal-alignment: center; vertical-alignment: center; } lan-port-field := FlatSurface { visible: root.lan-mode; x: lan-ip-picker.x + lan-ip-picker.width + 2px; y: 2px; width: lan-port-metrics.preferred-width * local-address-field.lan-font-scale + 4px; height: parent.height - 4px; background: lan-port-editor.has-focus ? ControlStyle.input-focused : lan-port-hover.has-hover ? ControlStyle.hover : transparent; border-radius: UiRadius.control; outline-color: lan-port-editor.has-focus ? UiSurface.focus : transparent; animate background, outline-color { duration: 120ms; } lan-port-hover := TouchArea { enabled: root.lan-mode && !root.lan-password-pending; mouse-cursor: text; FocusScope { width: parent.width; height: parent.height; key-pressed(event) => { if event.text == Key.Escape { root.lan-apply-pending = false; root.lan-port-dirty = false; root.lan-local-port = root.lan-listener-port; root.lan-endpoint-error = ""; focus-sink.focus(); return accept; } return reject; } lan-port-editor := TextInput { x: 2px; width: parent.width - 4px; height: parent.height; text <=> root.lan-local-port; font-size: 18px * local-address-field.lan-font-scale; font-weight: 600; input-type: InputType.number; color: WorkbenchStyle.text; enabled: root.lan-mode && !root.lan-password-pending; accessible-label: GalleryStrings.texts[75]; vertical-alignment: center; horizontal-alignment: left; selection-foreground-color: white; selection-background-color: WorkbenchStyle.accent; changed has-focus => { root.lan-port-editing = self.has-focus; // Opening the warning also moves focus: do not // treat that as another attempt to save. if !self.has-focus && root.lan-port-dirty && root.lan-access-warning == "" { root.lan-apply-pending = true; } } edited => { root.lan-port-dirty = true; root.lan-endpoint-error = ""; root.copy-feedback = CopyFeedback.idle; } accepted => { if root.apply-lan-endpoint() { focus-sink.focus(); } } } } } } copy-button := WorkbenchButton { x: parent.width - 42px; y: 2px; width: 36px; quiet: true; icon: root.copy-feedback == CopyFeedback.success ? FontAwesomeIcons.check : FontAwesomeIcons.copy; icon-color: root.copy-feedback == CopyFeedback.success ? WorkbenchStyle.success : WorkbenchStyle.muted; tooltip: root.lan-mode ? GalleryStrings.texts[76] : GalleryStrings.texts[77]; enabled: !root.lan-mode || root.lan-listener-address != ""; tooltip-suppressed: root.copy-feedback != CopyFeedback.idle; // Let other controls receive the original click; moving // focus away from Copy dismisses its transient feedback. changed has-focus => { if !self.has-focus { root.copy-feedback = CopyFeedback.idle; } } clicked => { if root.lan-mode && root.lan-port-dirty && !root.apply-lan-endpoint() { return; } let copied = root.lan-mode ? root.lan-copy-text(root.lan-listener-address) : root.copy-local-id(); root.copy-feedback = copied ? CopyFeedback.success : CopyFeedback.failed; copy-timer.restart(); } } private property edit-hint: !root.lan-mode || !root.sponsorship-unobstructed || root.lan-password-pending ? "" : lan-ip-picker.has-hover ? GalleryStrings.texts[78] : lan-port-hover.has-hover && !lan-port-editor.has-focus ? GalleryStrings.texts[79] : ""; private property edit-hint-visible; changed edit-hint => { local-address-field.edit-hint-visible = false; } Timer { interval: 450ms; running: local-address-field.edit-hint != "" && !local-address-field.edit-hint-visible; triggered => { local-address-field.edit-hint-visible = true; } } if local-address-field.edit-hint != "" && local-address-field.edit-hint-visible: Rectangle { x: clamp((lan-ip-picker.has-hover ? lan-ip-picker.x + lan-ip-picker.width / 2 : lan-port-field.x + lan-port-field.width / 2) - self.width / 2, 0px, parent.width - self.width); y: parent.height + 6px; width: min(parent.width, edit-hint-text.preferred-width + 20px); height: max(28px, edit-hint-text.preferred-height + 12px); background: #24324a; border-radius: UiRadius.control; z: 20; edit-hint-text := Text { x: 10px; y: 6px; width: parent.width - 20px; height: parent.height - 12px; text: local-address-field.edit-hint; font-size: 12px; color: white; wrap: word-wrap; horizontal-alignment: center; vertical-alignment: center; } } } Text { x: 16px; y: 138px; height: 18px; text: UiStrings.password; font-size: 12px; color: WorkbenchStyle.muted; } FlatSurface { x: 16px; y: 158px; width: parent.width - 32px; height: 36px; background: ControlStyle.input-surface; border-radius: UiRadius.control; outline-color: UiSurface.edge; Text { x: 12px; width: parent.width - 132px; height: parent.height; text: root.active-password == "" ? "—" : root.active-password-visible ? root.active-password : "••••••"; font-size: 20px; font-weight: 500; letter-spacing: 2px; color: WorkbenchStyle.text; vertical-alignment: center; } SurfaceSeparator { x: parent.width - 118px; y: 8px; width: 1px; height: parent.height - 16px; vertical: true; outline-color: UiSurface.separator; } WorkbenchButton { x: parent.width - 112px; y: 0px; width: 36px; quiet: true; icon: root.active-password-visible ? FontAwesomeIcons.eye : FontAwesomeIcons.eye-slash; tooltip: root.active-password-visible ? GalleryStrings.texts[80] : GalleryStrings.texts[81]; clicked => { if root.lan-mode { root.lan-password-visible = !root.lan-password-visible; } else { root.toggle-password-visibility(); } } } WorkbenchButton { x: parent.width - 74px; y: 0px; width: 36px; quiet: true; icon: FontAwesomeIcons.rotate; tooltip: UiStrings.refresh-password; enabled: root.lan-mode ? !root.lan-password-pending : !root.password-change-pending; clicked => { if root.lan-mode { root.lan-reset-password(); } else { root.refresh-password(); } } } WorkbenchButton { x: parent.width - 36px; y: 0px; width: 36px; quiet: true; icon: FontAwesomeIcons.pen; tooltip: GalleryStrings.texts[82]; enabled: root.lan-mode ? !root.lan-password-pending : !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: root.lan-mode ? GalleryStrings.texts[83] : GalleryStrings.texts[84]; font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; } Text { x: 16px; y: 68px; height: 18px; text: root.lan-mode ? GalleryStrings.texts[85] : UiStrings.remote-id; font-size: 12px; color: WorkbenchStyle.muted; } FlatSurface { x: 16px; y: 90px; width: parent.width - 32px; height: 40px; background: remote-id-editor.has-focus ? ControlStyle.input-focused : ControlStyle.input-surface; border-radius: UiRadius.control; outline-color: root.connection-input-error != "" ? UiSurface.error : remote-id-editor.has-focus ? UiSurface.focus : UiSurface.edge; outline-focused: remote-id-editor.has-focus || root.connection-input-error != ""; animate outline-color { duration: 120ms; } if root.remote-id-input == "": Text { x: 12px; width: parent.width - 24px; height: parent.height; text: root.lan-mode ? "192.168.1.20:52888" : GalleryStrings.texts[86]; font-size: 13px; color: ControlStyle.input-placeholder; vertical-alignment: center; } remote-id-editor := TextInput { x: 12px; width: parent.width - 24px; height: parent.height; text <=> root.remote-id-input; font-size: root.lan-mode ? 18px : 24px; font-weight: 600; letter-spacing: root.lan-mode ? 0px : 1px; input-type: root.lan-mode ? InputType.text : InputType.number; color: WorkbenchStyle.text; accessible-label: root.lan-mode ? GalleryStrings.texts[85] : UiStrings.remote-id; enabled: !root.connection-pending && !root.lan-connecting; vertical-alignment: center; selection-foreground-color: white; selection-background-color: WorkbenchStyle.accent; edited => { root.connection-input-error = ""; // Enforce the digit limit before the next frame can // render the text from typing, paste, or replacement. let formatted = root.lan-mode ? self.text : root.format-remote-id(self.text); self.text = formatted; // Formatted IDs are ASCII, so these are UTF-8 offsets. self.set-selection-offsets( formatted.character-count, formatted.character-count); } accepted => { root.submit-connection(); } } } Text { x: 16px; y: 133px; width: parent.width - 32px; height: 24px; text: root.connection-input-error != "" ? root.connection-input-error : root.lan-mode ? GalleryStrings.texts[87] : ""; font-size: 11px; color: root.connection-input-error != "" ? #be3d42 : WorkbenchStyle.muted; 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 || root.lan-connecting ? GalleryStrings.texts[56] : UiStrings.connect; enabled: !root.connection-pending && !root.lan-connecting; clicked => { root.submit-connection(); } } } } // Recent connections: previews, presence badges, and separate primary/overflow actions. recent-panel := TouchArea { y: 260px; height: parent.height - self.y; // This observer survives model resets that destroy hovered cards // before their has-hover changed handler can dismiss the details. private property preview-hovered: root.window-active && self.has-hover && root.recent-tooltip-index >= 0 && root.recent-tooltip-index < root.recent-connections.length && root.recent-connections[root.recent-tooltip-index].remote-id == root.recent-tooltip-id && self.mouse-x >= max(recent-list.x + recent-scroll.x, recent-list.x + recent-scroll.x + recent-scroll.viewport-x + root.recent-tooltip-card-x) && self.mouse-x < min(recent-list.x + recent-scroll.x + recent-scroll.width, recent-list.x + recent-scroll.x + recent-scroll.viewport-x + root.recent-tooltip-card-x + root.recent-preview-width) && self.mouse-y >= recent-list.y + recent-scroll.y && self.mouse-y < recent-list.y + recent-scroll.y + root.recent-preview-height; changed preview-hovered => { if !self.preview-hovered { root.recent-tooltip-visible = false; } } 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 { visible: !root.lan-mode; x: parent.width - self.width - 20px; y: 0px; width: 155px; height: 32px; normal-background: white; normal-outline-color: UiSurface.edge; hover-outline-color: UiSurface.hover; focus-outline-color: WorkbenchStyle.accent; model: root.recent-groups; current-index <=> root.recent-group-index; selected(index) => { root.recent-group-selected(index); } } recent-list := FlatSurface { x: 20px; y: 36px; width: parent.width - 40px; height: 142px; // A light flat surface groups the cards and their scrollbar. background: root.recent-connections.length > 0 ? #f5f7fb : transparent; outline-enabled: root.recent-connections.length > 0; outline-color: UiSurface.edge; 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.lan-mode ? GalleryStrings.texts[88] : root.recent-group-index == 0 ? GalleryStrings.texts[89] : GalleryStrings.texts[90]; color: WorkbenchStyle.text; font-size: 14px; font-weight: 600; } Text { x: 84px; y: 68px; width: parent.width - 104px; height: 20px; text: root.lan-mode ? GalleryStrings.texts[91] : root.recent-group-index == 0 ? GalleryStrings.texts[92] : GalleryStrings.texts[93]; color: WorkbenchStyle.muted; font-size: 12px; } } recent-scroll := ScrollView { visible: root.recent-connections.length > 0; x: 6px; y: 5px; width: parent.width - 12px; height: parent.height - 6px; horizontal-stretch: 1; changed viewport-x => { root.recent-tooltip-visible = false; root.copy-feedback = CopyFeedback.idle; } HorizontalLayout { padding: 0px; padding-bottom: 10px; spacing: 12px; alignment: start; for connection[index] in root.recent-connections: connection-card := Rectangle { width: root.recent-preview-width; height: 126px; background: transparent; private property available: connection.lan || connection.online || connection.connected; private property content-width: self.width; private property scroll-offset: recent-scroll.viewport-x; private property device-id: connection.remote-id; private property group-index: root.recent-group-index; changed scroll-offset => { card-menu.close-menu(); } changed device-id => { card-menu.close-menu(); } changed group-index => { card-menu.close-menu(); } Rectangle { width: connection-card.content-width; height: 124px; border-radius: UiRadius.surface; background: white; clip: true; Rectangle { x: 0px; y: 0px; width: parent.width; height: root.recent-preview-height; background: !connection-card.available ? DeviceCardStyle.offline-preview : connection.platform == "windows" ? #296ebc : connection.platform == "macos" ? #7d57b9 : connection.platform == "linux" ? #ad591a : connection.platform == "ios" ? #19786f : #667282; if connection.has-thumbnail: Image { width: parent.width; height: parent.height; source: connection.thumbnail; image-fit: cover; // Keep the saved preview recognizable under the offline gray. opacity: connection-card.available ? 1 : 0.16; } if !connection.has-thumbnail: Text { x: 8px; width: parent.width - 16px; height: parent.height; text: connection.platform == "windows" ? "Windows" : connection.platform == "macos" ? "macOS" : connection.platform == "linux" ? "Linux" : connection.platform == "ios" ? "iOS" : UiStrings.unknown-platform; color: connection-card.available ? white : DeviceCardStyle.offline-placeholder; font-size: 20px; font-weight: 600; horizontal-alignment: center; vertical-alignment: center; overflow: elide; } Rectangle { x: 8px; y: 8px; width: presence-label.preferred-width + 24px; height: 20px; background: #fffffff0; border-radius: self.height / 2; Rectangle { x: 8px; y: (parent.height - self.height) / 2; width: 5px; height: 5px; border-radius: self.height / 2; background: connection-card.available ? WorkbenchStyle.success : WorkbenchStyle.muted; } presence-label := Text { x: 17px; width: self.preferred-width; height: parent.height; text: connection.lan ? "LAN" : connection-card.available ? UiStrings.online : UiStrings.offline; color: connection-card.available ? WorkbenchStyle.success : WorkbenchStyle.muted; font-size: 10px; font-weight: 500; vertical-alignment: center; } } } Rectangle { x: 0px; y: 80px; width: parent.width; height: 44px; background: white; SurfaceSeparator { x: 0px; y: 0px; width: parent.width; height: 1px; outline-color: UiSurface.separator; } Text { x: 12px; y: 5px; height: 19px; width: parent.width - 56px; text: connection.display-name; color: WorkbenchStyle.text; font-size: 13px; font-weight: 600; overflow: elide; vertical-alignment: center; } Text { x: 12px; y: 24px; height: 16px; width: parent.width - 56px; text: connection.lan ? connection.endpoint : connection.remote-id; font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; } } // The software renderer only clips rectangular bounds. // This outer border masks the four corners there too. Rectangle { x: -parent.border-radius; y: -parent.border-radius; width: parent.width + 2 * parent.border-radius; height: parent.height + 2 * parent.border-radius; border-width: parent.border-radius; border-radius: 2 * parent.border-radius; border-color: recent-list.background; } } card-touch := TouchArea { width: connection-card.content-width; height: 124px; mouse-cursor: pointer; clicked => { root.copy-feedback = CopyFeedback.idle; root.recent-connect(connection.remote-id); } image-touch := TouchArea { x: 0px; y: 0px; width: parent.width; height: 80px; private property 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.copy-feedback = CopyFeedback.idle; root.recent-connect(connection.remote-id); } function show-details() { if !root.window-active || !self.has-hover { return; } // Leave the menu trigger and its popup free of device details. if card-menu.menu-open || (self.mouse-x >= card-menu.x - 4px && self.mouse-y <= card-menu.y + card-menu.height + 4px) { root.recent-tooltip-visible = false; return; } root.recent-tooltip-name = connection.display-name; root.recent-tooltip-index = index; root.recent-tooltip-card-x = connection-card.x; root.recent-tooltip-host = connection.host-name; root.recent-tooltip-id = connection.remote-id; root.recent-tooltip-groups = self.group-names; root.recent-tooltip-online = self.available; root.recent-tooltip-pointer-x = recent-panel.x + recent-list.x + recent-scroll.x + recent-scroll.viewport-x + connection-card.x + card-touch.x + self.x + self.mouse-x; root.recent-tooltip-pointer-y = recent-panel.y + recent-list.y + recent-scroll.y + recent-scroll.viewport-y + connection-card.y + card-touch.y + self.y + self.mouse-y; root.recent-tooltip-visible = true; } changed has-hover => { if self.has-hover { self.show-details(); } else if root.recent-tooltip-id == connection.remote-id { root.recent-tooltip-visible = false; } } // moved only reports pressed pointer movement; // track both coordinates during ordinary hover. changed mouse-x => { self.show-details(); } changed mouse-y => { self.show-details(); } } card-menu := GalleryMenuButton { x: parent.width - self.width - 8px; y: 8px; width: 24px; height: 24px; compact-trigger: true; compact-menu: true; enabled: !root.gallery-mode; text: GalleryStrings.texts[94]; menu-width: root.compact-language ? 116px : 156px; items: [ {label: GalleryStrings.texts[35], enabled: true}, {label: GalleryStrings.texts[30], enabled: !connection.lan}, {label: GalleryStrings.texts[95], enabled: true}, ]; item-icons: [FontAwesomeIcons.pen, FontAwesomeIcons.tags, FontAwesomeIcons.trash-can]; destructive-index: 2; opened => { root.recent-tooltip-visible = false; root.copy-feedback = CopyFeedback.idle; } selected(action) => { root.copy-feedback = CopyFeedback.idle; if action == 0 { root.alias-remote-id = connection.remote-id; root.alias-input = connection.display-name; root.alias-open = true; } else if action == 1 { root.open-device-groups(connection.remote-id, connection.display-name); } else if action == 2 { root.delete-remote-id = connection.remote-id; root.delete-from-gallery = false; root.delete-open = true; } } } WorkbenchButton { x: parent.width - self.width - 8px; y: 88px; width: 28px; height: 28px; soft: true; primary: !connection.connected; destructive: connection.connected; icon: connection.connected ? FontAwesomeIcons.xmark : FontAwesomeIcons.arrow-right-long; tooltip: connection.connected ? GalleryStrings.texts[96] : UiStrings.connect; tooltip-above: true; clicked => { root.copy-feedback = CopyFeedback.idle; root.recent-tooltip-visible = false; if connection.connected { root.disconnect-remote-id = connection.remote-id; root.disconnect-device-name = connection.display-name; root.disconnect-open = true; } else { root.recent-connect(connection.remote-id); } } } } FlatSurface { width: parent.width; height: 124px; border-radius: UiRadius.surface; outline-color: card-touch.has-hover || card-menu.menu-open ? UiSurface.hover : UiSurface.edge; animate outline-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; } } // Show availability for the selected connection mode. if !root.gallery-mode: Rectangle { y: parent.height - 28px; height: 28px; background: white; SurfaceSeparator { y: 0px; height: 1px; outline-color: UiSurface.separator; } Rectangle { x: 20px; y: 11px; width: 6px; height: 6px; border-radius: 3px; background: root.lan-mode ? (root.lan-message != "" || root.lan-listener-error != "" ? #d6932b : root.lan-listening ? WorkbenchStyle.success : WorkbenchStyle.muted) : root.signal-tls-error || root.signal-reconnecting ? #d6932b : root.signal-connected ? WorkbenchStyle.success : #d65252; } Text { x: 34px; y: 0px; height: parent.height; width: parent.width - 160px; overflow: elide; text: root.lan-mode ? (root.lan-message != "" ? LanStrings.error-message(root.lan-message) : root.lan-listener-error != "" ? (root.lan-listener-address == "" ? "" : root.lan-listener-address + " · ") + LanStrings.error-message(root.lan-listener-error) : root.lan-listening ? GalleryStrings.texts[97] : root.lan-password-pending ? GalleryStrings.texts[98] : GalleryStrings.texts[99]) : 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.lan-mode || (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.lan-mode && !root.signal-connected: WorkbenchButton { x: parent.width - self.width - 20px; y: (parent.height - self.height) / 2; width: max(64px, reconnect-label-metrics.preferred-width + 22px); height: 22px; text: UiStrings.signal-reconnect; text-size: 11px; text-color: WorkbenchStyle.accent; clicked => { root.copy-feedback = CopyFeedback.idle; root.signal-reconnect-requested(); } reconnect-label-metrics := Text { visible: false; text: UiStrings.signal-reconnect; font-size: 11px; font-weight: 500; } } } } // 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 && recent-panel.preview-hovered) && !root.connection-dialog-open && !root.lan-open: device-tip := FlatSurface { 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 has-host-name: self.host-name != "" && self.host-name != self.device-name; private property status-color: self.online ? WorkbenchStyle.success : WorkbenchStyle.muted; private property label-width: max(tooltip-id-label.preferred-width, tooltip-group-label.preferred-width); 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)); // Fit the text instead of reserving empty space for short device names. width: min(220px, parent.width - 16px, max(160px, tooltip-name.preferred-width + 24px, self.has-host-name ? tooltip-host.preferred-width + 24px : 0px, tooltip-name-label.preferred-width + tooltip-status.width + 32px, self.label-width + max(tooltip-id.preferred-width, tooltip-groups.preferred-width) + 32px)); height: tooltip-groups.y + tooltip-groups.height + 10px; background: WorkbenchStyle.surface; outline-color: UiSurface.edge; border-radius: UiRadius.menu; z: 20; tooltip-name-label := Text { x: 12px; y: 10px; width: tooltip-status.x - self.x - 8px; height: 18px; text: UiStrings.device-name; color: WorkbenchStyle.muted; font-size: 10px; vertical-alignment: center; overflow: elide; } tooltip-status := Rectangle { x: parent.width - self.width - 12px; y: 10px; width: tooltip-status-text.preferred-width + 14px; height: 18px; background: device-tip.online ? #e9f6ef : WorkbenchStyle.field-surface; border-radius: self.height / 2; tooltip-status-text := Text { x: 7px; width: parent.width - 14px; height: parent.height; text: device-tip.online ? UiStrings.online : device-tip.presence-known ? UiStrings.offline : GalleryStrings.texts[100]; color: device-tip.status-color; font-size: 10px; font-weight: 600; vertical-alignment: center; } } tooltip-name := Text { x: 12px; y: 32px; width: parent.width - 24px; height: self.preferred-height; text: device-tip.device-name; color: WorkbenchStyle.text; font-size: 12px; font-weight: 600; wrap: word-wrap; } tooltip-host := Text { x: 12px; y: tooltip-name.y + tooltip-name.height + 3px; width: parent.width - 24px; height: device-tip.has-host-name ? self.preferred-height : 0px; visible: device-tip.has-host-name; text: device-tip.host-name; color: WorkbenchStyle.muted; font-size: 10px; wrap: word-wrap; } tooltip-divider := SurfaceSeparator { x: 12px; y: tooltip-name.y + tooltip-name.height + (device-tip.has-host-name ? tooltip-host.height + 3px : 0px) + 8px; width: parent.width - 24px; height: 1px; outline-color: UiSurface.separator; } tooltip-id-label := Text { x: 12px; y: tooltip-divider.y + 7px; width: device-tip.label-width; height: 16px; text: UiStrings.remote-id; color: WorkbenchStyle.muted; font-size: 10px; vertical-alignment: center; } tooltip-id := Text { x: 20px + device-tip.label-width; y: tooltip-id-label.y; width: parent.width - self.x - 12px; height: 16px; text: device-tip.device-id; color: WorkbenchStyle.text; font-size: 11px; font-weight: 600; horizontal-alignment: right; vertical-alignment: center; } tooltip-group-label := Text { x: 12px; y: tooltip-groups.y; width: device-tip.label-width; height: 16px; text: GalleryStrings.texts[101]; color: WorkbenchStyle.muted; font-size: 10px; vertical-alignment: center; } tooltip-groups := Text { x: 20px + device-tip.label-width; y: tooltip-id-label.y + tooltip-id-label.height + 4px; width: parent.width - self.x - 12px; height: max(16px, self.preferred-height); text: device-tip.group-names == "" ? GalleryStrings.texts[31] : device-tip.group-names; color: device-tip.group-names == "" ? WorkbenchStyle.muted : WorkbenchStyle.accent; font-size: 10px; wrap: word-wrap; horizontal-alignment: right; vertical-alignment: center; } } // Dismiss the application menu when clicking anywhere outside of it. if !root.gallery-mode && root.menu-open: TouchArea { x: 0px; y: 0px; width: parent.width; height: parent.height; z: 9; clicked => { root.menu-open = false; focus-sink.focus(); } } // Expand downward from the application toolbar. if !root.gallery-mode && root.menu-open: FlatSurface { 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; outline-color: UiSurface.edge; 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; SurfaceSeparator { x: 8px; y: 3px; width: parent.width - 16px; height: 1px; outline-color: UiSurface.separator; } } 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; SurfaceSeparator { x: 8px; y: 3px; width: parent.width - 16px; height: 1px; outline-color: UiSurface.separator; } } menu-about := Rectangle { height: 30px; background: about-touch.pressed ? WorkbenchStyle.pressed-surface : about-touch.has-hover ? WorkbenchStyle.hover-surface : transparent; border-width: 0px; border-radius: UiRadius.control; animate background { duration: 120ms; } accessible-role: button; accessible-label: UiStrings.about + (root.update-available ? " · " + UiStrings.update-available : ""); accessible-action-default => { about-touch.clicked(); } menu-about-label := Text { x: 8px; height: parent.height; text: UiStrings.about; color: WorkbenchStyle.text; font-size: 12px; vertical-alignment: center; accessible-role: none; } menu-update-badge := Rectangle { visible: root.update-available; x: parent.width - self.width - 7px; y: (parent.height - self.height) / 2; width: menu-update-label.preferred-width + 12px; height: 18px; border-radius: 9px; background: #dbeafe; accessible-role: none; menu-update-label := Text { text: UiStrings.new-version; font-size: 11px; font-weight: 400; color: #1d4ed8; horizontal-alignment: center; vertical-alignment: center; accessible-role: none; } } about-touch := TouchArea { mouse-cursor: pointer; clicked => { root.menu-open = false; root.about-open = true; } } } } } if root.messages-open: Rectangle { background: #15223850; z: 40; TouchArea { clicked => { focus-sink.focus(); } } MessagePanel { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: min(592px, parent.width - 32px); height: min(400px, parent.height - 24px); items: root.announcements; unread-count: root.announcement-unread; total: root.announcement-total; loading: root.announcement-loading; failed: root.announcement-failed; loaded: root.announcement-loaded; read-save-failed: root.announcement-read-save-failed; delete-failed: root.announcement-delete-failed; connected: root.signal-connected; selected-id: root.announcement-selected-id; selected-title: root.announcement-selected-title; selected-revision: root.announcement-selected-revision; selected-body: root.announcement-selected-body; selected-date: root.announcement-selected-date; selected-read: root.announcement-selected-read; open-message(id, revision) => { root.open-announcement(id, revision); } open-link(url) => { root.open-announcement-link(url); } delete-message(id, revision) => { root.delete-announcement(id, revision); } load-more => { root.load-more-announcements(); } read-all => { root.read-all-announcements(); } close => { root.messages-open = false; } } } copy-timer := Timer { interval: root.copy-feedback == CopyFeedback.failed ? 4s : 2500ms; running: root.copy-feedback != CopyFeedback.idle; triggered => { root.copy-feedback = CopyFeedback.idle; } } // Float above the workbench while keeping modal dialogs in front. if !root.gallery-mode && root.copy-feedback != CopyFeedback.idle: WorkbenchNotice { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2 - 24px; width-limit: min(440px, parent.width - 40px); error: root.copy-feedback == CopyFeedback.failed; icon: self.error ? FontAwesomeIcons.xmark : FontAwesomeIcons.check; text: self.error ? GalleryStrings.texts[102] : root.lan-mode ? GalleryStrings.texts[103] : UiStrings.copied; z: 30; } // Reset password modal. if root.reset-password-open: Rectangle { background: #00000042; z: 40; TouchArea { clicked => { focus-sink.focus(); } } DialogSurface { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: root.compact-language ? 260px : 320px; height: reset-content.preferred-height; reset-content := VerticalLayout { padding: 24px; spacing: 16px; Text { text: UiStrings.new-password; color: WorkbenchStyle.text; font-size: 13px; font-weight: 500; wrap: word-wrap; min-height: 22px; vertical-alignment: center; } PasswordInput { width: parent.width - 48px; height: 38px; text-size: 16px; text <=> root.new-password-input; max-length: 6; error: root.reset-password-invalid; } if root.reset-password-invalid: Text { text: UiStrings.invalid-password; color: #b42318; font-size: 12px; wrap: word-wrap; } HorizontalLayout { spacing: 12px; height: 34px; PrimaryButton { text: UiStrings.ok; height: 34px; horizontal-stretch: 1; enabled: root.lan-mode ? !root.lan-password-pending : !root.password-change-pending; 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; height: 34px; horizontal-stretch: 1; 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-outline-color: UiSurface.edge; 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.texts[104] : GalleryStrings.texts[105]) : 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; } } } } } } // Confirm the selected recent session before disconnecting it. if root.disconnect-open: Rectangle { background: #14233b66; z: 48; TouchArea { scroll-event(event) => { accept } } FocusScope { width: parent.width; height: parent.height; key-pressed(event) => { if event.text == Key.Escape { root.dismiss-disconnect(); return accept; } if event.text == Key.Tab { if disconnect-cancel.has-focus { disconnect-confirm.focus(); } else { disconnect-cancel.focus(); } return accept; } return reject; } Timer { interval: 1ms; running: true; triggered => { self.running = false; // Defer focus until after Slint's accessibility update. disconnect-cancel.focus(); } } DialogSurface { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: min(420px, parent.width - 40px); height: disconnect-content.preferred-height; outline-color: UiSurface.edge; TouchArea { scroll-event(event) => { accept } } disconnect-content := VerticalLayout { padding: 24px; spacing: 16px; Text { text: GalleryStrings.texts[106]; color: WorkbenchStyle.text; font-size: 18px; font-weight: 600; wrap: word-wrap; } VerticalLayout { spacing: 6px; Text { text: root.disconnect-device-name; color: WorkbenchStyle.text; font-size: 14px; font-weight: 600; overflow: elide; } Text { text: root.disconnect-remote-id; color: WorkbenchStyle.muted; font-size: 12px; } } Text { text: GalleryStrings.texts[107]; color: WorkbenchStyle.muted; font-size: 13px; wrap: word-wrap; } HorizontalLayout { alignment: end; spacing: 8px; disconnect-cancel := WorkbenchButton { text: UiStrings.cancel; clicked => { root.dismiss-disconnect(); } } disconnect-confirm := WorkbenchButton { text: GalleryStrings.texts[96]; destructive: true; clicked => { let remote-id = root.disconnect-remote-id; root.dismiss-disconnect(); root.recent-disconnect(remote-id); } } } } } } } // 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 || root.lan-open: Rectangle { background: #15223850; z: 48; TouchArea { clicked => { if !root.lan-open { focus-sink.focus(); } } } WorkbenchCard { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 340px; height: (root.prompt-password-required && !root.prompt-validating ? 236px : 168px) + max(0px, connection-prompt-text.preferred-height - 46px); connection-prompt-text := Text { x: 24px; y: 24px; width: parent.width - 48px; height: max(46px, self.preferred-height); text: root.prompt-status-text; color: WorkbenchStyle.text; font-size: 14px; wrap: word-wrap; horizontal-alignment: center; vertical-alignment: center; } if !root.lan-open && root.connection-password-required && !root.connection-validating: PasswordInput { x: (parent.width - self.width) / 2; y: connection-prompt-text.y + connection-prompt-text.height + 16px; width: 164px; height: 36px; text-size: 16px; border-radius: UiRadius.control; text <=> root.connection-password; accepted => { root.submit-connection-password(); } } if root.lan-open && !root.lan-connecting: PasswordInput { x: (parent.width - self.width) / 2; y: connection-prompt-text.y + connection-prompt-text.height + 16px; width: 164px; height: 36px; text-size: 16px; max-length: 6; border-radius: UiRadius.control; text <=> root.lan-access-password; accepted => { root.submit-connection-password(); } } if root.prompt-password-required && !root.prompt-validating: Rectangle { x: (parent.width - self.width) / 2; y: connection-prompt-text.y + connection-prompt-text.height + 64px; width: remember-label.preferred-width + 29px; height: 21px; if root.lan-open: ImGuiCheckBox { x: 0px; y: 0px; checked <=> root.lan-remember-password; } if !root.lan-open: 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 => { if root.lan-open { root.lan-remember-password = !root.lan-remember-password; } else { 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.prompt-password-required || root.prompt-pending: WorkbenchButton { text: UiStrings.cancel; clicked => { root.cancel-connection-prompt(); } } if root.prompt-password-required && !root.prompt-validating: WorkbenchButton { primary: true; text: UiStrings.ok; enabled: !root.lan-open || (root.lan-retry-after == 0 && root.lan-access-password.character-count == 6); clicked => { root.submit-connection-password(); } } if !root.prompt-password-required && !root.prompt-pending: WorkbenchButton { primary: true; text: UiStrings.ok; clicked => { root.connection-acknowledge(); } } } } } // macOS permission gate. On other platforms this remains hidden. if root.permission-dialog-open: Rectangle { background: #15223850; z: 50; TouchArea { clicked => { focus-sink.focus(); } } DialogSurface { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: min(400px, parent.width - 40px); height: min(permission-content.preferred-height, parent.height - 32px); background: #f8fafc; outline-color: UiSurface.edge; TouchArea { clicked => { focus-sink.focus(); } } permission-scroll := ScrollView { width: parent.width; height: parent.height; viewport-width: self.visible-width; viewport-height: permission-content.preferred-height; horizontal-scrollbar-policy: always-off; vertical-scrollbar-policy: as-needed; Rectangle { width: permission-scroll.viewport-width; height: permission-scroll.viewport-height; permission-content := VerticalLayout { padding: 24px; spacing: 20px; VerticalLayout { spacing: 8px; Text { text: UiStrings.request-permissions; font-size: 20px; font-weight: 600; color: WorkbenchStyle.text; wrap: word-wrap; } Text { text: UiStrings.permission-required; font-size: 12px; color: WorkbenchStyle.muted; wrap: word-wrap; } } VerticalLayout { spacing: 12px; for permission[index] in [ {label: UiStrings.accessibility-permission, granted: root.accessibility-granted, description: GalleryStrings.texts[108]}, {label: UiStrings.screen-recording-permission, granted: root.screen-recording-granted, description: GalleryStrings.texts[109]}, ]: WorkbenchCard { height: max(80px, permission-labels.preferred-height + 32px); permission-labels := VerticalLayout { x: 16px; y: 16px; width: parent.width - permission-action.width - 48px; height: parent.height - 32px; spacing: 6px; alignment: center; Text { text: permission.label; font-size: 13px; font-weight: 600; color: WorkbenchStyle.text; wrap: word-wrap; } Text { text: permission.description; font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap; } } permission-action := Rectangle { x: parent.width - self.width - 16px; y: (parent.height - self.height) / 2; width: max(96px, permission-action-metrics.preferred-width + 24px); height: 36px; permission-action-metrics := Text { visible: false; font-size: 13px; font-weight: 600; text: GalleryStrings.texts[110]; } if !permission.granted: WorkbenchButton { width: parent.width; height: parent.height; primary: true; enabled: !root.permission-migration-running; text: permission-action-metrics.text; accessible-label: permission.label + ": " + self.text; clicked => { if index == 0 { root.request-accessibility-permission(); } else { root.request-screen-recording-permission(); } } } if permission.granted: HorizontalLayout { alignment: center; spacing: 6px; Text { text: FontAwesomeIcons.check; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 12px; color: WorkbenchStyle.success; vertical-alignment: center; accessible-role: none; } Text { text: GalleryStrings.texts[111]; font-size: 12px; color: WorkbenchStyle.success; vertical-alignment: center; } } } } } VerticalLayout { spacing: 12px; SurfaceSeparator { height: 1px; outline-color: UiSurface.separator; } if root.permission-migration-running || root.permission-migration-failed: Text { text: root.permission-migration-running ? UiStrings.permission-migration-running : UiStrings.permission-migration-failed; font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap; } Text { text: GalleryStrings.texts[112]; font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap; } HorizontalLayout { alignment: end; spacing: 8px; WorkbenchButton { text: root.accessibility-granted && root.screen-recording-granted && !root.permission-migration-running && !root.permission-migration-failed ? UiStrings.ok : UiStrings.permission-later; clicked => { root.dismiss-permissions(); } } if root.permission-migration-failed: retry-migration := WorkbenchButton { width: max(112px, retry-migration-label.preferred-width + 24px); primary: true; text: UiStrings.retry-permission-migration; clicked => { root.retry-permission-migration(); } retry-migration-label := Text { visible: false; accessible-role: none; text: retry-migration.text; font-size: 13px; font-weight: 600; } } } } } } } } } // 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; outline-color: UiSurface.edge; 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; } // Match the compact connection-mode switch, centered in the tab row. FlatSurface { x: settings-dialog.content-inset; y: 62px; width: parent.width - 2 * settings-dialog.content-inset; height: 28px; background: WorkbenchStyle.field-surface; border-radius: UiRadius.control; border-width: 1px; border-color: WorkbenchStyle.accent; outline-color: UiSurface.edge; Rectangle { x: root.settings-tab == 1 ? parent.width / 2 + 1px : 2px; y: 2px; width: (parent.width - 6px) / 2; height: parent.height - 4px; background: WorkbenchStyle.accent; border-radius: UiRadius.control; animate x { duration: 180ms; easing: ease-in-out; } } for label[index] in [GalleryStrings.texts[113], GalleryStrings.texts[54]]: Rectangle { x: index == 1 ? parent.width / 2 + 1px : 2px; y: 2px; width: (parent.width - 6px) / 2; height: parent.height - 4px; accessible-role: button; accessible-label: label; accessible-action-default => { tab-touch.clicked(); } Text { text: label; font-size: 12px; font-weight: 500; color: root.settings-tab == index ? white : WorkbenchStyle.muted; animate color { duration: 180ms; } 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 - (root.settings-save-error == "" ? 0px : 28px); 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.texts[114]; rows: 1; SettingsRow { label: UiStrings.language; divider: false; SettingsSelect { x: parent.width - 192px; y: 4px; width: 180px; model: root.language-options; current-index <=> root.language-index; } } } SettingsGroup { title: GalleryStrings.texts[115]; rows: 2; 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; } } pause-video-hover := TouchArea { height: 36px; function update-hint() { root.pause-video-hint-visible = self.has-hover; root.pause-video-hint-x = self.absolute-position.x + self.mouse-x; root.pause-video-hint-y = self.absolute-position.y + self.mouse-y; } changed has-hover => { self.update-hint(); } pointer-event(event) => { self.update-hint(); } SettingsToggleRow { width: parent.width; label: UiStrings.pause-video-when-minimized; checked <=> root.pause-video-when-minimized; divider: false; } } } SettingsGroup { title: GalleryStrings.texts[116]; rows: 4; SettingsToggleRow { label: GalleryStrings.texts[117]; checked <=> root.ip-direct-enabled-draft; } SettingsToggleRow { label: GalleryStrings.texts[206]; checked <=> root.lan-listen-all-draft; enabled: root.ip-direct-enabled-draft; } SettingsToggleRow { label: UiStrings.force-relay; checked <=> root.force-relay-enabled; enabled: !root.settings-session-active; } SettingsToggleRow { label: UiStrings.self-hosted; label-reserve: self-host-edit.width + 84px; checked <=> root.self-hosted-enabled; enabled: !root.settings-session-active; divider: false; self-host-edit := SettingsButton { x: parent.width - self.width - 60px; y: 4px; width: self-host-edit-metrics.preferred-width + 24px; height: 28px; text: GalleryStrings.texts[118]; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } self-host-edit-metrics := Text { visible: false; accessible-role: none; text: parent.text; font-size: 12px; } } } } SettingsGroup { title: GalleryStrings.texts[120]; rows: 5 + (root.mac-permission-settings-visible ? 1 : 0) + (root.portable-service-settings-visible ? 1 : 0); SettingsToggleRow { label: UiStrings.privacy-on-connect; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; } SettingsToggleRow { label: UiStrings.privacy-local-unlock; checked <=> root.privacy-local-unlock-allowed; enabled: root.privacy-setting-available; } SettingsToggleRow { label: UiStrings.lock-on-disconnect; checked <=> root.lock-on-disconnect-enabled; enabled: root.lock-on-disconnect-available; } SettingsToggleRow { label: TrustStrings.only; checked <=> root.trusted-devices-only; } SettingsRow { label: TrustStrings.title; divider: root.mac-permission-settings-visible || root.portable-service-settings-visible; SettingsButton { x: parent.width - self.width - 12px; y: 4px; width: max(104px, self.label-width); height: 28px; text: TrustStrings.manage; clicked => { root.trusted-devices-open = true; root.trust-action("open", "", ""); } } } if root.mac-permission-settings-visible: SettingsRow { label: UiStrings.system-permissions; divider: false; SettingsButton { x: parent.width - self.width - 12px; y: 4px; width: max(104px, self.label-width); height: 28px; text: TrustStrings.manage; clicked => { root.open-permissions(); } } } if root.portable-service-settings-visible: SettingsRow { label: UiStrings.service-settings-label; divider: false; label-reserve: service-button.width + 24px; service-button := SettingsButton { x: parent.width - self.width - 12px; y: 4px; width: max(104px, self.label-width); height: 28px; text: root.portable-service-installed || root.portable-service-needs-repair ? UiStrings.repair-service : UiStrings.install-service; enabled: !root.portable-service-installing; clicked => { root.open-portable-service(); } } } } Text { text: GalleryStrings.texts[121]; font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap; } SettingsGroup { title: GalleryStrings.texts[122]; 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.texts[123]; rows: 2; if !root.autostart-system-managed: SettingsToggleRow { label: UiStrings.autostart; checked <=> root.autostart-enabled; enabled: root.autostart-available && !root.settings-session-active; } if root.autostart-system-managed: SettingsToggleRow { label: UiStrings.autostart; checked: root.autostart-status == AutostartStatus.enabled; toggle-on-click: false; clicked => { root.autostart-guide-open = true; } } SettingsToggleRow { label: UiStrings.daemon; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; divider: false; } } if root.service-autostart-platform: Text { text: root.service-autostart-hint; wrap: word-wrap; font-size: 11px; color: WorkbenchStyle.muted; } } } } 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 - (root.settings-save-error == "" ? 0px : 28px); 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; } if root.settings-save-error != "": Text { x: settings-dialog.content-inset; y: parent.height - 92px; width: parent.width - 2 * settings-dialog.content-inset; height: 24px; text: root.settings-save-error; font-size: 11px; color: #c53e3e; wrap: word-wrap; } SurfaceSeparator { x: settings-dialog.content-inset; y: parent.height - 64px; width: parent.width - 2 * settings-dialog.content-inset; height: 1px; outline-color: UiSurface.separator; } 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: max(80px, self.label-width); text: UiStrings.cancel; clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; } } SettingsButton { width: max(92px, self.label-width); primary: true; text: GalleryStrings.texts[34]; clicked => { if root.save-settings() { root.settings-open = false; root.self-host-settings-open = false; } } } } if root.window-active && root.settings-tab == 0 && root.pause-video-hint-visible: Rectangle { // Keep the immediate hint outside ScrollView's clipping area. width: min(260px, parent.width - 16px); height: max(28px, pause-video-hint-text.preferred-height + 12px); x: clamp(root.pause-video-hint-x - settings-dialog.absolute-position.x, 8px, parent.width - self.width - 8px); private property pointer-y: root.pause-video-hint-y - settings-dialog.absolute-position.y; y: self.pointer-y + 8px + self.height <= parent.height - 8px ? self.pointer-y + 8px : self.pointer-y - self.height - 8px; background: #24324a; border-radius: UiRadius.control; z: 20; pause-video-hint-text := Text { x: 10px; y: 6px; width: parent.width - 20px; height: parent.height - 12px; text: UiStrings.pause-video-when-minimized-hint; font-size: 12px; font-weight: 400; color: white; wrap: word-wrap; horizontal-alignment: left; vertical-alignment: center; } } } } if root.autostart-guide-open && root.settings-open: Rectangle { background: #15223835; z: 49; TouchArea { clicked => { focus-sink.focus(); } } DialogSurface { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: min(460px, parent.width - 32px); height: min(380px, parent.height - 24px); background: #f8fafc; outline-color: UiSurface.edge; TouchArea { clicked => { focus-sink.focus(); } } Text { x: 24px; y: 18px; width: parent.width - 48px; height: 26px; text: GalleryStrings.texts[124]; font-size: 18px; font-weight: 600; color: WorkbenchStyle.text; } ScrollView { x: 24px; y: 58px; width: parent.width - 48px; height: parent.height - 120px; viewport-width: self.visible-width; horizontal-scrollbar-policy: always-off; viewport-height: guide-content.preferred-height; guide-content := VerticalLayout { width: parent.width; padding: 0px; spacing: 12px; Text { text: root.autostart-status-text; color: WorkbenchStyle.accent; font-size: 13px; wrap: word-wrap; } Text { text: root.autostart-status == AutostartStatus.unsupported ? GalleryStrings.texts[125] : root.autostart-status == AutostartStatus.failed ? GalleryStrings.texts[126] : GalleryStrings.texts[127]; color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap; } if root.autostart-status != AutostartStatus.unsupported: Text { text: GalleryStrings.texts[128]; color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap; } } } SettingsButton { x: 24px; y: parent.height - 48px; width: 112px; text: GalleryStrings.texts[129]; enabled: root.autostart-status != AutostartStatus.checking; clicked => { root.refresh-autostart-status(); } } SettingsButton { x: parent.width - 116px; y: parent.height - 48px; width: 92px; primary: true; text: UiStrings.ok; clicked => { root.autostart-guide-open = false; } } } } if root.trusted-devices-open: TrustedDevicesDialog { z: 51; devices: root.trusted-devices; candidates: root.trust-candidates; local-fingerprint: root.device-fingerprint; error: root.trusted-devices-error; notice: root.trusted-devices-notice; copy-error: root.trust-copy-error; copy-notice: root.trust-copy-notice; pairing-input <=> root.trust-pairing-input; close => { root.trust-action("close", "", ""); root.trusted-devices-open = false; } action(action, id, key) => { root.trust-action(action, id, key); } } 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; outline-color: UiSurface.edge; background: #f8fafc; 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-outline-color: UiSurface.edge; 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-outline-color: UiSurface.edge; 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; } SurfaceSeparator { x: 18px; y: parent.height - 66px; width: parent.width - 36px; height: 1px; outline-color: UiSurface.separator; } 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 - 124px - self.width; y: parent.height - 50px; width: max(80px, self.label-width); text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } } } } if root.sponsorship-open && root.sponsorship-unobstructed: Rectangle { background: #15223850; z: 38; TouchArea { clicked => { focus-sink.focus(); } } SettingsDialog { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: min(480px, parent.width - 40px); height: self.content-top + sponsorship-content.preferred-height + 24px; title: UiStrings.sponsorship-title; show-close-button: false; sponsorship-content := VerticalLayout { x: 24px; y: parent.content-top; width: parent.width - 48px; height: self.preferred-height; padding: 0px; spacing: 20px; Text { text: UiStrings.sponsorship-message; color: WorkbenchStyle.text; font-size: 14px; wrap: word-wrap; // Match message bodies' 1.5em line height for wrapped text. font-family: "CrossDesk Message Spacing"; } SponsorshipSection { show-title: false; open-afdian => { root.open-afdian-sponsor(); } open-kofi => { root.open-kofi-sponsor(); } open-github => { root.open-github-sponsor(); } } Text { text: UiStrings.sponsorship-dismiss-hint; color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap; font-family: "CrossDesk Message Spacing"; } SurfaceSeparator { height: 1px; outline-color: UiSurface.separator; } HorizontalLayout { alignment: end; SettingsButton { width: max(80px, sponsorship-close-metrics.preferred-width + 24px); primary: true; text: MessageStrings.close; clicked => { root.dismiss-sponsorship(); } sponsorship-close-metrics := Text { visible: false; text: MessageStrings.close; font-size: 12px; } } } } } } if root.about-open: Rectangle { // Use the same bounds throughout the About navigation stack. private property dialog-width: min(480px, self.width - 40px); private property dialog-height: min(520px, self.height - 32px); background: #15223850; z: 46; TouchArea { clicked => { focus-sink.focus(); } } if root.about-page != AboutPage.overview: AboutPanel { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: parent.dialog-width; min-height: 0px; max-height: parent.height - 32px; height: parent.dialog-height; page <=> root.about-page; component-index <=> root.about-component-index; components: root.about-components; openfec-index: root.about-openfec-index; revision: root.about-revision; source-url: root.about-source-url; build-url: root.about-build-url; back => { root.about-back(); } open-link(url) => { root.open-about-link(url); } } if root.about-page == AboutPage.overview: SettingsDialog { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: parent.dialog-width; // The overlay excludes any custom titlebar on every platform. min-height: 0px; max-height: parent.height - 32px; // Keep the dialog stable as update status and release details change. height: parent.dialog-height; title: UiStrings.about; close-label: MessageStrings.close; close => { root.about-open = false; } // Preserve section spacing and keep actions visible when longer // translations need scrolling inside a small window. about-scroll := ScrollView { x: 24px; y: parent.content-top; width: parent.width - 48px + 22px; height: about-footer.y - self.y - 16px; viewport-width: self.visible-width; viewport-height: about-content.preferred-height; // Keep the position when navigation recreates the overview. viewport-y <=> root.about-scroll-y; horizontal-scrollbar-policy: always-off; vertical-scrollbar-policy: as-needed; about-content := VerticalLayout { min-width: 0px; width: about-scroll.width; height: self.preferred-height; // Keep the expanding scrollbar outside the content. padding: 0px; padding-right: 22px; spacing: 16px; FlatSurface { height: max(80px, product-info.preferred-height + 32px); background: WorkbenchStyle.surface; border-radius: UiRadius.surface; outline-color: UiSurface.edge; Image { x: 16px; y: (parent.height - self.height) / 2; width: 56px; height: 56px; source: @image-url("../../../resources/linux/crossdesk_128x128.png"); image-fit: contain; } product-info := VerticalLayout { x: 88px; y: 16px; width: parent.width - 104px; height: parent.height - 32px; alignment: center; spacing: 5px; Text { text: "CrossDesk"; font-size: 22px; font-weight: 600; color: WorkbenchStyle.text; } Text { text: UiStrings.version + " " + root.current-version; color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap; } } } if root.update-available: FlatSurface { height: update-summary.preferred-height + 24px; background: WorkbenchStyle.field-surface; border-radius: UiRadius.control; outline-color: UiSurface.edge; update-summary := VerticalLayout { x: 12px; y: 12px; width: parent.width - 24px; height: self.preferred-height; padding: 0px; spacing: 10px; if root.update-available: HorizontalLayout { spacing: 12px; height: max(32px, version-link.preferred-height); Rectangle { min-width: 0px; horizontal-stretch: 1; accessible-role: button; accessible-label: UiStrings.release-notes + " " + root.latest-version; accessible-action-default => { version-touch.clicked(); } version-link := Text { width: parent.width; height: parent.height; text: UiStrings.update-available + ": " + root.latest-version; color: version-touch.has-hover ? #254cc0 : WorkbenchStyle.accent; font-size: 13px; font-weight: 500; wrap: word-wrap; vertical-alignment: center; } version-touch := TouchArea { mouse-cursor: pointer; clicked => { root.about-open = false; root.update-open = true; } } } SettingsButton { width: max(116px, release-notes-button-metrics.preferred-width + 24px); text: UiStrings.view-release-notes; clicked => { root.about-open = false; root.update-open = true; } release-notes-button-metrics := Text { visible: false; text: UiStrings.view-release-notes; font-size: 12px; } } } } } SponsorshipSection { open-afdian => { root.open-afdian-sponsor(); } open-kofi => { root.open-kofi-sponsor(); } open-github => { root.open-github-sponsor(); } } VerticalLayout { spacing: 5px; Text { text: UiStrings.copyright; color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap; } Text { text: UiStrings.license; color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap; } } AboutNavigationGroup { AboutNavigationRow { grouped: true; title: AboutStrings.software-license; detail: "GPL v3"; clicked => { root.about-component-index = 0; root.about-page = AboutPage.component; } } AboutRowSeparator { } AboutNavigationRow { grouped: true; title: AboutStrings.components; clicked => { root.about-page = AboutPage.components; } } AboutRowSeparator { } AboutNavigationRow { grouped: true; title: AboutStrings.source; clicked => { root.about-page = AboutPage.source; } } } } } about-footer := VerticalLayout { x: 24px; y: parent.height - self.height - 24px; width: parent.width - 48px; height: self.preferred-height; spacing: 16px; SurfaceSeparator { height: 1px; outline-color: UiSurface.separator; } if root.update-status-text != "": Text { text: root.update-status-text; color: root.update-check-status == UpdateCheckStatus.failed ? #b3261e : root.update-check-status == UpdateCheckStatus.up-to-date ? WorkbenchStyle.success : WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap; } HorizontalLayout { alignment: end; spacing: 8px; if root.update-available: WorkbenchButton { width: max(80px, about-update-button-metrics.preferred-width + 24px); height: 32px; primary: true; text-size: 12px; text: root.update-action-text; enabled: !root.update-install-busy; clicked => { root.open-download(); root.about-open = false; root.update-open = true; } about-update-button-metrics := Text { visible: false; text: root.update-action-text; font-size: 12px; } } SettingsButton { width: max(116px, check-button-metrics.preferred-width + 24px); text: UiStrings.check-for-updates; enabled: root.update-check-status != UpdateCheckStatus.checking; clicked => { root.check-for-updates(); } check-button-metrics := Text { visible: false; text: UiStrings.check-for-updates; font-size: 12px; } } SettingsButton { width: 80px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } } } } } } // Update notification modal. if root.update-open && root.update-available: Rectangle { background: #14233b45; z: 47; TouchArea { clicked => { focus-sink.focus(); } } DialogSurface { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: min(480px, parent.width - 40px); // Use the same content area for sizing and centering the dialog. height: min(488px, parent.height - 32px); border-radius: UiRadius.surface; outline-color: UiSurface.edge; 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-website(); } update-website-label := Text { text: UiStrings.access-website + "https://crossdesk.cn"; color: website-touch.has-hover ? #254cc0 : WorkbenchStyle.accent; font-size: 13px; vertical-alignment: center; } website-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-website(); } } } } } VerticalLayout { vertical-stretch: 1; spacing: 8px; Text { height: 18px; text: UiStrings.release-notes; color: WorkbenchStyle.text; font-size: 12px; font-weight: 600; vertical-alignment: center; } FlatSurface { background: WorkbenchStyle.field-surface; outline-color: UiSurface.edge; border-radius: UiRadius.control; // Inset the viewport so text and the scrollbar stay clear // of the rounded frame, including at either scroll limit. update-notes-scroll := ScrollView { x: 12px; y: 12px; width: parent.width - 24px; height: parent.height - 24px; viewport-width: self.width - 12px; viewport-height: update-notes-content.preferred-height; horizontal-scrollbar-policy: always-off; vertical-scrollbar-policy: as-needed; update-notes-content := VerticalLayout { width: update-notes-scroll.viewport-width; alignment: start; spacing: 8px; padding-bottom: 6px; for block in root.release-note-blocks: Rectangle { height: update-note.preferred-height + (block.section-gap ? 6px : 0px); update-note := StyledText { y: block.section-gap ? 6px : 0px; // Measure wrapped paragraphs at the viewport // width before stacking the following block. width: update-notes-scroll.viewport-width; height: self.preferred-height; text: block.content; default-color: WorkbenchStyle.text; default-font-size: 13px; // Match the 1.5em line height of message bodies. default-font-family: "CrossDesk Message Spacing"; } } } } } } SponsorshipSection { open-afdian => { root.open-afdian-sponsor(); } open-kofi => { root.open-kofi-sponsor(); } open-github => { root.open-github-sponsor(); } } VerticalLayout { vertical-stretch: 0; spacing: 16px; SurfaceSeparator { height: 1px; outline-color: UiSurface.separator; } if root.update-install-status != "": Text { text: root.update-install-status; font-size: 12px; color: WorkbenchStyle.muted; wrap: word-wrap; max-height: 64px; overflow: elide; } HorizontalLayout { alignment: end; spacing: 8px; WorkbenchButton { width: 88px; height: 34px; text: UiStrings.later; clicked => { root.cancel-update(); root.update-open = false; } } if root.update-install-failed && root.update-install-available: WorkbenchButton { width: max(110px, manual-download-metrics.preferred-width + 24px); height: 34px; enabled: !root.update-install-busy; text: UiStrings.download; clicked => { root.open-manual-download(); } manual-download-metrics := Text { visible: false; text: UiStrings.download; font-size: 12px; } } WorkbenchButton { width: max(110px, update-action-metrics.preferred-width + 24px); height: 34px; primary: true; enabled: !root.update-install-busy; text: root.update-action-text; clicked => { root.open-download(); } update-action-metrics := Text { visible: false; text: root.update-action-text; font-size: 12px; } } } } } } } // Keep confirmation inside the app instead of relying on external native // dialogs and their platform-specific button labels. if root.update-confirm-open: Rectangle { background: #14233b66; z: 48; TouchArea {} FocusScope { width: parent.width; height: parent.height; key-pressed(event) => { if event.text == Key.Escape { root.update-confirm-open = false; return accept; } if event.text == Key.Tab { if update-confirm-cancel.has-focus { update-confirm-install.focus(); } else { update-confirm-cancel.focus(); } return accept; } return reject; } Timer { interval: 1ms; running: true; triggered => { self.running = false; // Defer focus until after Slint's accessibility update. // Return defaults to cancelling this disruptive action. update-confirm-cancel.focus(); } } DialogSurface { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: min(440px, parent.width - 40px); height: update-confirm-content.preferred-height; border-radius: UiRadius.surface; outline-color: UiSurface.edge; TouchArea {} update-confirm-content := VerticalLayout { padding: 24px; spacing: 20px; Text { text: UiStrings.install-update; color: WorkbenchStyle.text; font-size: 18px; font-weight: 600; wrap: word-wrap; } Text { text: UiStrings.install-update-confirm; color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap; } HorizontalLayout { alignment: end; spacing: 8px; update-confirm-cancel := WorkbenchButton { text: UiStrings.cancel; clicked => { root.update-confirm-open = false; } } update-confirm-install := WorkbenchButton { width: max(112px, confirm-label-metrics.preferred-width + 24px); text: UiStrings.install-update; primary: true; enabled: root.update-install-available && !root.update-install-busy; clicked => { root.confirm-update(); } confirm-label-metrics := Text { visible: false; text: UiStrings.install-update; font-size: 13px; } } } } } } } // 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; outline-color: UiSurface.edge; 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; } } FlatSurface { background: #fff8eb; outline-color: UiSurface.edge; 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; } } } FlatSurface { background: #f3f7fd; outline-color: UiSurface.edge; 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; } } } SurfaceSeparator { height: 1px; outline-color: UiSurface.separator; } 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: #15223835; z: 50; TouchArea { clicked => { focus-sink.focus(); } } ServicePromptDialog { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: min(480px, parent.width - 32px); available-height: parent.height - 32px; title: root.portable-service-needs-repair ? UiStrings.service-repair-title : UiStrings.service-setup-title; message: root.portable-service-needs-repair ? UiStrings.service-repair-message : UiStrings.service-setup-message; status: root.portable-service-status; busy: root.portable-service-installing; succeeded: root.portable-service-succeeded; show-opt-out: !root.portable-service-succeeded && !root.portable-service-needs-repair; opt-out-label: UiStrings.do-not-remind; opt-out <=> root.portable-service-do-not-remind; primary-text: root.portable-service-succeeded ? UiStrings.ok : root.portable-service-needs-repair ? UiStrings.repair-service : UiStrings.install-service; secondary-text: root.portable-service-succeeded ? "" : UiStrings.cancel; primary-action => { if root.portable-service-succeeded { root.acknowledge-portable-service(); } else { root.install-portable-service(); } } secondary-action => { root.cancel-portable-service(); } } } if root.portable-service-suppressed-notice-open: Rectangle { background: #15223835; z: 51; TouchArea { clicked => { focus-sink.focus(); } } ServicePromptDialog { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: min(440px, parent.width - 32px); available-height: parent.height - 32px; title: UiStrings.notification; message: UiStrings.service-suppressed-message; primary-text: UiStrings.ok; primary-action => { 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: FlatSurface { x: 0px; y: 0px; width: root.width; height: root.height; background: transparent; outline-color: UiSurface.edge; border-radius: root.wayland-titlebar ? root.wayland-corner-radius : 0px; z: 200; } } // Each explicit validation failure opens one modal, without a focus loop. if root.lan-access-warning != "": Rectangle { width: parent.width; height: parent.height; background: #00000042; z: 300; TouchArea { scroll-event(event) => { accept } } lan-warning-focus := FocusScope { width: parent.width; height: parent.height; key-pressed(event) => { if event.text == Key.Escape || event.text == Key.Return || event.text == " " { root.dismiss-lan-access-warning(); } else if event.text == Key.Tab { self.focus(); } return accept; } Timer { interval: 1ms; running: true; triggered => { self.running = false; // Defer focus until after Slint's accessibility update. root.menu-open = false; lan-ip-picker.close-menu(); lan-warning-focus.focus(); } } DialogSurface { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: min(root.compact-language ? 270px : 320px, parent.width - 40px); // Let short messages stay compact; longer translations grow // naturally without stretching the gap above the action. height: lan-warning-content.preferred-height; TouchArea { scroll-event(event) => { accept } } lan-warning-content := VerticalLayout { padding: 20px; spacing: 20px; padding-top: 28px; padding-bottom: 28px; if root.lan-access-warning-endpoint != "": Text { text: root.lan-access-warning-endpoint; font-size: 12px; font-weight: 600; color: WorkbenchStyle.text; horizontal-alignment: center; wrap: word-wrap; } lan-warning-message := Rectangle { private property <[string]> lines: LanStrings.access-error-lines(root.lan-access-warning); height: warning-lines.preferred-height; vertical-stretch: 0; accessible-role: text; accessible-label: LanStrings.error-message(root.lan-access-warning); warning-lines := VerticalLayout { width: parent.width; height: self.preferred-height; spacing: 6px; for line in (lan-warning-message.lines[0] != "" ? lan-warning-message.lines : [LanStrings.error-message(root.lan-access-warning)]): Text { text: line; font-size: ImGuiFontStyle.base; color: #30343b; horizontal-alignment: center; wrap: word-wrap; accessible-role: none; } } } HorizontalLayout { height: 30px; alignment: center; PrimaryButton { text: UiStrings.ok; accessible-role: button; accessible-label: UiStrings.ok; accessible-action-default => { root.dismiss-lan-access-warning(); } clicked => { root.dismiss-lan-access-warning(); } } } } } } } }