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

3861 lines
200 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { 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 <string> window-minimize: "Minimize";
in-out property <string> window-close: "Close window";
in-out property <string> local-desktop: "Local Desktop";
in-out property <string> remote-desktop: "Remote Desktop";
in-out property <string> recent-connections: "Recent Connections";
in-out property <string> local-id: "Local ID";
in-out property <string> device-name: "Device Name";
in-out property <string> remote-id: "Remote ID";
in-out property <string> online: "Online";
in-out property <string> offline: "Offline";
in-out property <string> password: "Password";
in-out property <string> refresh-password: "Refresh random password";
in-out property <string> copied: "Local ID copied to clipboard";
in-out property <string> connect: "Connect";
in-out property <string> settings: "Settings";
in-out property <string> about: "About";
in-out property <string> sponsorship-title: "Sponsorship";
in-out property <string> sponsorship-message;
in-out property <string> sponsorship-dismiss-hint;
in-out property <string> sponsor-via-afdian: "AFDIAN";
in-out property <string> ok: "OK";
in-out property <string> cancel: "Cancel";
in-out property <string> save: "Save";
in-out property <string> new-password: "New password (6 characters)";
in-out property <string> invalid-password: "Use exactly 6 letters (A–Z, a–z) or digits (0–9).";
in-out property <string> edit-alias: "Connection alias";
in-out property <string> delete-connection: "Delete connection";
in-out property <string> confirm-delete: "Delete this recent connection?";
in-out property <string> language: "Language";
in-out property <string> codec: "Video codec";
in-out property <string> pause-video-when-minimized: "Pause video when minimized";
in-out property <string> pause-video-when-minimized-hint: "Pause video reception when the control window is minimized to reduce local decoding resource usage.";
in-out property <string> force-relay: "Force relay";
in-out property <string> self-hosted: "Self-hosted server";
in-out property <string> autostart: "Start with system";
in-out property <string> daemon: "Run in background";
in-out property <string> save-remote-previews: "Save remote screen previews";
in-out property <string> unknown-platform: "Unknown platform";
in-out property <string> privacy-local-unlock: "Allow privacy screen unlock while controlled";
in-out property <string> privacy-on-connect: "Privacy screen on connect";
in-out property <string> lock-on-disconnect: "Lock screen on disconnect";
in-out property <string> file-save-path: "File save path";
in-out property <string> default-desktop: "Desktop";
in-out property <string> server-host: "Server host";
in-out property <string> server-port: "Signal port";
in-out property <string> version: "Version";
in-out property <string> copyright: "© 2026 by JUNKUN DI. All rights reserved.";
in-out property <string> license: "Licensed under GNU GPL v3.";
in-out property <string> signal-connected: "Signal server connected";
in-out property <string> signal-disconnected: "Signal server disconnected";
in-out property <string> tls-error: "Server certificate invalid or expired. Retrying automatically.";
in-out property <string> signal-reconnect: "Reconnect";
in-out property <string> signal-reconnecting: "Reconnecting...";
in-out property <string> update-available: "A new version is available";
in-out property <string> new-version: "New version";
in-out property <string> check-for-updates: "Check for updates";
in-out property <string> checking-for-updates: "Checking for updates…";
in-out property <string> up-to-date: "You’re up to date";
in-out property <string> update-check-failed: "Check failed. Please try again later.";
in-out property <string> release-notes: "Release notes";
in-out property <string> view-release-notes: "Release notes";
in-out property <string> later: "Later";
in-out property <string> download: "Download";
in-out property <string> install-update: "Install update";
in-out property <string> retry-update: "Retry update";
in-out property <string> install-update-confirm;
in-out property <string> input-password: "Input password";
in-out property <string> reinput-password: "Incorrect password, please try again";
in-out property <string> remember-password: "Remember password";
in-out property <string> validate-password: "Validating password";
in-out property <string> request-permissions: "Request permissions";
in-out property <string> permission-required: "The application requires the following permissions:";
in-out property <string> screen-recording-permission: "Screen recording permission";
in-out property <string> accessibility-permission: "Accessibility permission";
in-out property <string> system-permissions: "System permissions";
in-out property <string> permission-migration-running: "Migrating permissions…";
in-out property <string> permission-migration-failed;
in-out property <string> retry-permission-migration: "Retry migration";
in-out property <string> permission-later: "Later";
in-out property <string> wayland-notice-title: "Wayland Session Notice";
in-out property <string> wayland-notice-subtitle;
in-out property <string> wayland-permission-title;
in-out property <string> wayland-notice-message;
in-out property <string> wayland-recommendation-title;
in-out property <string> wayland-recommendation-message;
in-out property <string> wayland-notice-acknowledge: "Got it";
in-out property <string> service-setup-title: "Install CrossDesk Service";
in-out property <string> service-setup-message: "Administrator permission is required to control this device while it is locked.";
in-out property <string> service-settings-label: "Lock-screen control service";
in-out property <string> install-service: "Install";
in-out property <string> service-installed: "Installed";
in-out property <string> repair-service: "Repair";
in-out property <string> service-repair-title: "Repair CrossDesk Service";
in-out property <string> service-repair-message;
in-out property <string> do-not-remind: "Do not remind again";
in-out property <string> notification: "Notification";
in-out property <string> service-suppressed-message: "You will not be reminded again. You can install the service later in Settings.";
in-out property <string> codec-h264: "H.264";
in-out property <string> codec-av1: "AV1";
in-out property <string> self-hosted-settings: "Self-hosted server settings";
in-out property <string> access-website: "Website: ";
in-out property <string> release-date-label: "Release Date: ";
}
// Keep sponsor labels, order, and sizing consistent across dialogs.
component SponsorshipSection inherits VerticalLayout {
in property <bool> 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 <string> prompt;
in property <color> text-color: #30343b;
in property <length> text-size: ImGuiFontStyle.base;
in property <int> text-weight: 400;
// Mirror Text's intrinsic layout constraints exactly. This makes the
// wrapper invisible to VerticalLayout: siblings retain the same geometry
// they had when the Text item was a direct child.
min-height: prompt-label.min-height;
preferred-height: prompt-label.preferred-height;
max-height: prompt-label.max-height;
vertical-stretch: prompt-label.vertical-stretch;
prompt-label := Text {
y: 10px;
width: parent.width;
text: root.prompt;
color: root.text-color;
font-size: root.text-size;
font-weight: root.text-weight;
wrap: word-wrap;
horizontal-alignment: center;
}
}
// 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 <string> title;
in property <string> message;
in property <string> status;
in property <string> primary-text;
in property <string> secondary-text;
in property <string> opt-out-label;
in property <bool> busy: false;
in property <bool> succeeded: false;
in property <bool> show-opt-out: false;
in-out property <bool> opt-out: false;
in property <length> 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 <string> text;
in property <int> max-length: 0;
in property <length> text-size: ImGuiFontStyle.base;
in property <bool> error: false;
private property <string> 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 <bool> gallery-mode: false;
in-out property <bool> gallery-simplified: false;
changed gallery-simplified => {
root.reset-gallery-transient();
root.gallery-action("view-mode", "");
focus-sink.focus();
}
in-out property <bool> gallery-network-open: false;
in property <int> gallery-resolution-index: 0;
in property <int> gallery-frame-rate-index: 0;
in property <int> gallery-bandwidth-index: 3;
in-out property <int> gallery-resolution-draft: 0;
in-out property <int> gallery-frame-rate-draft: 0;
in-out property <int> gallery-bandwidth-draft: 3;
in property <string> gallery-settings-error;
in property <[GalleryNetworkDevice]> gallery-network-devices;
changed gallery-network-open => {
if root.gallery-network-open { root.menu-open = false; root.gallery-action("network-open", ""); }
}
in property <bool> gallery-fullscreen: false;
in property <bool> gallery-transitioning: false;
changed gallery-transitioning => {
if root.gallery-transitioning {
// Preserve the preview's keyboard focus when only changing size.
if !root.gallery-controlling { focus-sink.focus(); }
root.menu-open = false;
root.recent-tooltip-visible = false;
}
}
in-out property <string> connection-input-error;
in property <bool> gallery-controlling: false;
in property <int> gallery-remote-cursor-shape: 0;
in property <bool> gallery-group-controlling: false;
out property <float> gallery-video-width: gallery.video-width;
out property <float> gallery-video-height: gallery.video-height;
out property <float> gallery-preview-width: gallery.preview-width;
out property <float> gallery-preview-height: gallery.preview-height;
callback gallery-pointer(PointerEventButton, PointerEventKind, length, length);
callback gallery-scroll(length, length, length, length);
callback gallery-key(string, bool, bool, bool, bool, bool);
in property <[GalleryTile]> gallery-tiles;
in property <[string]> gallery-groups;
in-out property <int> gallery-group-index: 0;
out property <int> gallery-columns: gallery.columns;
out property <int> gallery-rows: gallery.rows;
in-out property <float> gallery-tile-width: 216;
in-out property <float> gallery-focus-rail-height: 70.875;
out property <bool> gallery-resizing: gallery.resizing || gallery.focus-resizing;
in property <int> gallery-page: 0;
in property <int> gallery-pages: 1;
in property <int> gallery-count: 0;
in property <int> gallery-connected: 0;
in property <int> gallery-selected: 0;
in property <bool> gallery-selected-audio-available: false;
in property <bool> gallery-selected-audio-muted: false;
in property <int> gallery-hidden-selected: 0;
in property <string> gallery-notice;
in property <[GalleryActionDevice]> gallery-selected-devices;
in property <GalleryTile> gallery-focused;
in-out property <bool> gallery-batch: false;
in-out property <bool> gallery-adding: false;
in-out property <bool> gallery-importing: false;
in property <[GalleryRecentDevice]> gallery-recent-devices;
in property <int> gallery-recent-selected: 0;
in property <int> gallery-recent-available: 0;
in property <string> gallery-import-group;
callback open-gallery();
callback toggle-gallery-fullscreen();
callback gallery-action(string, string);
callback gallery-swap(string, string) -> bool;
callback gallery-add-device(string, string, string, bool) -> bool;
callback gallery-import-recent(string) -> bool;
in-out property <bool> device-group-open: false;
private property <string> device-group-id;
private property <string> device-group-name;
in property <[DeviceGroupOption]> device-group-options;
in property <string> device-group-error;
in-out property <string> device-group-new-name;
callback device-group-action(string, string);
callback device-group-save(string) -> bool;
function open-device-groups(id: string, name: string) {
root.gallery-action("release", "");
focus-sink.focus();
root.recent-tooltip-visible = false;
root.device-group-id = id;
root.device-group-name = name;
root.device-group-new-name = "";
root.device-group-action("open", id);
root.device-group-open = true;
}
public function reset-gallery-transient() { gallery.reset-transient(); root.menu-open = false; root.gallery-network-open = false; }
in property <bool> custom-titlebar: false;
in property <bool> wayland-titlebar: false;
in property <bool> window-active: true;
private property <length> titlebar-height:
root.custom-titlebar && !root.gallery-fullscreen ? (root.wayland-titlebar ? 32px : 31px) : 0px;
out property <length> minimum-window-width: 640px;
out property <length> minimum-window-height:
(root.gallery-mode ? 448px : 472px) + root.titlebar-height;
private property <length> wayland-corner-radius: 12px;
in property <string> local-id: "";
in property <string> local-password: "";
in property <bool> 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 <bool> lan-open;
changed lan-open => {
if !root.lan-open { root.lan-access-password = ""; }
if root.lan-open { root.gallery-action("release", ""); }
}
in-out property <bool> 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 <string> id-connection-draft;
private property <string> ip-connection-draft;
private property <bool> lan-password-visible: true;
private property <bool> active-password-visible: root.lan-mode ? root.lan-password-visible : root.password-visible;
private property <string> 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 <bool> lan-listening;
in-out property <bool> lan-listen-all: true;
in-out property <string> lan-listener-ip;
in-out property <bool> lan-listener-ipv6;
in-out property <string> lan-listener-port: "52888";
out property <string> 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 <int> lan-interface-index: 0;
in-out property <int> lan-interface-draft-index: 0;
in-out property <string> lan-local-port: "52888";
in-out property <bool> lan-port-editing;
in-out property <bool> lan-port-dirty;
in-out property <string> lan-endpoint-error;
in-out property <string> lan-access-warning;
in-out property <string> lan-access-warning-endpoint;
in-out property <string> 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 <bool> 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 <string> lan-remote-address;
in-out property <string> lan-remote-port: "52888";
in-out property <bool> lan-password-pending;
in-out property <bool> lan-connecting;
in-out property <int> lan-retry-after;
in-out property <string> lan-access-password;
in-out property <bool> lan-remember-password;
in-out property <string> lan-new-password;
in-out property <string> 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 <bool> prompt-password-required: root.lan-open || root.connection-password-required;
private property <bool> prompt-validating: root.lan-open ? root.lan-connecting : root.connection-validating;
private property <bool> prompt-pending: root.lan-open ? root.lan-connecting : root.connection-pending;
private property <string> 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 <string> remote-id-input: "";
in property <bool> password-visible: true;
in property <bool> password-change-pending: false;
in property <bool> signal-connected: false;
in property <bool> signal-reconnecting: false;
in property <bool> signal-tls-error: false;
in property <[RecentConnection]> recent-connections;
out property <length> recent-preview-width: max(1px, (recent-scroll.width - 24px) / 3);
out property <length> recent-preview-height: 80px;
in property <[string]> recent-groups: [GalleryStrings.texts[57], GalleryStrings.texts[31]];
in-out property <int> recent-group-index: 0;
callback recent-group-selected(int);
changed recent-group-index => {
root.recent-tooltip-visible = false;
recent-scroll.viewport-x = 0px;
}
in property <[AnnouncementItem]> announcements;
in property <int> announcement-unread;
private property <string> announcement-unread-label:
root.announcement-unread > 99 ? "99+" : "" + root.announcement-unread;
in property <int> announcement-total;
in property <bool> announcement-loading;
in property <bool> announcement-failed;
in property <bool> announcement-read-save-failed;
in property <bool> announcement-delete-failed;
in property <bool> announcement-loaded;
in-out property <bool> messages-open: false;
in property <string> announcement-selected-id;
in property <string> announcement-selected-revision;
in property <string> announcement-selected-title;
in property <styled-text> announcement-selected-body;
in property <string> announcement-selected-date;
in property <bool> announcement-selected-read;
callback open-announcement(string, string);
callback open-announcement-link(string);
callback delete-announcement(string, string);
callback refresh-announcements;
callback read-all-announcements;
callback load-more-announcements;
in property <bool> update-available: false;
in property <bool> update-install-available: false;
in property <bool> update-install-busy: false;
in property <bool> 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 <string> update-install-status: "";
private property <string> 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 <UpdateCheckStatus> update-check-status: idle;
private property <string> update-status-text:
root.update-check-status == UpdateCheckStatus.checking ? UiStrings.checking-for-updates :
root.update-check-status == UpdateCheckStatus.up-to-date ? UiStrings.up-to-date :
root.update-check-status == UpdateCheckStatus.failed ? UiStrings.update-check-failed : "";
in property <string> current-version: "";
in property <string> latest-version: "";
in property <string> release-name: "";
in property <[ReleaseNoteBlock]> release-note-blocks;
in property <string> release-date: "";
in-out property <string> offline-warning: "";
in property <bool> connection-dialog-open: false;
in property <string> connection-status-text: "";
in property <bool> connection-pending: false;
in property <bool> connection-password-required: false;
in property <bool> connection-validating: false;
in-out property <string> connection-password: "";
in-out property <bool> connection-remember-password: false;
in property <bool> permission-dialog-open: false;
in property <bool> mac-permission-settings-visible: false;
in property <bool> permission-migration-running: false;
in property <bool> permission-migration-failed: false;
in property <bool> screen-recording-granted: true;
in property <bool> accessibility-granted: true;
in-out property <bool> wayland-notice-open: false;
in property <bool> portable-service-settings-visible: false;
in property <bool> portable-service-dialog-open: false;
in property <bool> portable-service-suppressed-notice-open: false;
in property <bool> portable-service-installed: false;
in property <bool> portable-service-needs-repair: false;
in property <bool> portable-service-installing: false;
in property <bool> portable-service-succeeded: false;
in property <string> portable-service-status: "";
in-out property <bool> portable-service-do-not-remind: false;
in-out property <int> language-index: 10;
in property <[string]> language-options;
in-out property <int> codec-index: 0;
in-out property <bool> ip-direct-enabled-draft: true;
in-out property <bool> lan-listen-all-draft: true;
in property <string> settings-save-error;
in-out property <bool> force-relay-enabled: false;
in-out property <bool> self-hosted-enabled: false;
in-out property <bool> autostart-enabled: false;
in property <bool> autostart-available: true;
in property <bool> service-autostart-platform: false;
in property <string> service-autostart-hint;
in property <bool> autostart-system-managed: false;
in property <AutostartStatus> autostart-status: AutostartStatus.checking;
out property <string> autostart-status-text:
root.autostart-status == AutostartStatus.checking ? GalleryStrings.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 <bool> autostart-guide-open: false;
changed autostart-guide-open => { if root.autostart-guide-open { root.dialog-focus-pending = true; } }
in-out property <bool> daemon-enabled: false;
in-out property <bool> save-remote-previews: true;
in-out property <bool> pause-video-when-minimized: true;
// Show the preference on every platform; only adapted hosts can change it.
in property <bool> privacy-setting-available: false;
in-out property <bool> privacy-on-connect-enabled: true;
in-out property <bool> privacy-local-unlock-allowed: true;
in property <bool> lock-on-disconnect-available: false;
in-out property <bool> lock-on-disconnect-enabled: true;
in-out property <bool> trusted-devices-only: false;
in-out property <bool> trusted-devices-open: false;
in property <[TrustedDeviceEntry]> trusted-devices;
in property <[TrustedDeviceEntry]> trust-candidates;
in property <string> device-fingerprint;
in property <string> trusted-devices-error;
in property <string> trusted-devices-notice;
in property <string> trust-copy-error;
in property <string> trust-copy-notice;
in-out property <string> trust-pairing-input;
callback trust-action(string, string, string);
in-out property <string> file-save-path: "";
in-out property <string> server-host: "";
in-out property <string> server-port: "";
in-out property <string> self-host-settings-error: "";
in property <bool> settings-session-active: false;
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 <bool> menu-open: false;
in-out property <bool> settings-open: false;
in-out property <int> settings-tab: 0;
private property <length> settings-scroll-y: 0px;
private property <bool> pause-video-hint-visible: false;
private property <length> pause-video-hint-x: 0px;
private property <length> 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 <bool> self-host-settings-open: false;
changed self-host-settings-open => {
if !root.self-host-settings-open { root.self-host-settings-error = ""; }
}
in-out property <bool> about-open: false;
in-out property <AboutPage> about-page: overview;
private property <length> about-scroll-y: 0px;
in-out property <int> about-component-index: 0;
in property <[AboutComponent]> about-components;
in property <int> about-openfec-index: -1;
in property <string> about-revision;
in property <string> about-source-url;
in property <string> 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 <bool> sponsorship-open: false;
// Automatic prompts must not interrupt another dialog or menu.
private property <bool> 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 <bool> 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 <bool> update-open: root.update-available;
changed update-open => {
if !root.update-open { root.update-confirm-open = false; }
}
in-out property <bool> update-confirm-open: false;
in-out property <bool> reset-password-open: false;
changed reset-password-open => {
root.new-password-input = "";
root.reset-password-invalid = false;
}
in-out property <bool> reset-password-invalid: false;
in-out property <string> new-password-input: "";
in-out property <bool> alias-open: false;
private property <string> alias-remote-id: "";
in-out property <string> alias-input: "";
in-out property <bool> delete-open: false;
private property <string> delete-remote-id: "";
private property <bool> delete-from-gallery: false;
in-out property <bool> disconnect-open: false;
private property <string> disconnect-remote-id: "";
private property <string> disconnect-device-name: "";
function dismiss-disconnect() {
root.disconnect-open = false;
root.disconnect-remote-id = "";
root.disconnect-device-name = "";
focus-sink.focus();
}
private property <CopyFeedback> copy-feedback: idle;
private property <bool> compact-language: GalleryStrings.language == 0 || GalleryStrings.language == 3 || GalleryStrings.language == 4 || GalleryStrings.language == 5;
private property <bool> recent-tooltip-visible: false;
private property <int> recent-tooltip-index: -1;
private property <length> recent-tooltip-card-x: 0px;
private property <string> recent-tooltip-name: "";
private property <string> recent-tooltip-host: "";
private property <string> recent-tooltip-id: "";
private property <string> recent-tooltip-groups: "";
private property <bool> recent-tooltip-online: false;
private property <length> recent-tooltip-pointer-x: 0px;
private property <length> recent-tooltip-pointer-y: 0px;
private property <bool> 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 <int> phase: self.pressed ? 1 : 0;
changed phase => {
if self.pressed { root.copy-feedback = CopyFeedback.idle; }
root.main-title-drag(phase, self.mouse-x, self.mouse-y);
}
moved => {
if (self.pressed) {
root.main-title-drag(2, self.mouse-x, self.mouse-y);
}
}
}
wayland-minimize-button := Rectangle {
x: parent.width - 74px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: wayland-minimize-touch.pressed ? #c0bfbc
: wayland-minimize-touch.has-hover ? #deddda
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: 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 <int> phase: self.pressed ? 1 : 0;
changed phase => {
if self.pressed { root.copy-feedback = CopyFeedback.idle; }
root.main-title-drag(phase, self.mouse-x, self.mouse-y);
}
moved => { if (self.pressed) { root.main-title-drag(2, self.mouse-x, self.mouse-y); } }
}
minimize-button := Rectangle {
x: parent.width - 92px;
width: 46px;
height: parent.height;
background: minimize-touch.pressed ? #c7c7c7
: minimize-touch.has-hover ? #e5e5e5
: transparent;
accessible-role: button;
accessible-label: 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 <float> 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 <string> 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 <bool> 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 <bool> 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 <bool> available: connection.lan || connection.online || connection.connected;
private property <length> content-width: self.width;
private property <length> scroll-offset: recent-scroll.viewport-x;
private property <string> device-id: connection.remote-id;
private property <int> group-index: root.recent-group-index;
changed scroll-offset => { card-menu.close-menu(); }
changed device-id => { card-menu.close-menu(); }
changed group-index => { card-menu.close-menu(); }
Rectangle {
width: connection-card.content-width;
height: 124px;
border-radius: UiRadius.surface;
background: white;
clip: true;
Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: 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 <string> group-names: connection.group-names;
private property <bool> available: connection-card.available;
changed available => {
if self.has-hover && root.recent-tooltip-id == connection.remote-id {
root.recent-tooltip-online = self.available;
}
}
changed group-names => {
if self.has-hover && root.recent-tooltip-id == connection.remote-id {
root.recent-tooltip-groups = self.group-names;
}
}
clicked => { root.copy-feedback = CopyFeedback.idle; root.recent-connect(connection.remote-id); }
function show-details() {
if !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 <string> device-name: root.gallery-mode ? gallery.hovered-device.name : root.recent-tooltip-name;
private property <string> host-name: root.gallery-mode ? gallery.hovered-device.host-name : root.recent-tooltip-host;
private property <string> device-id: root.gallery-mode ? gallery.hovered-device.id : root.recent-tooltip-id;
private property <string> group-names: root.gallery-mode ? gallery.hovered-device.group-names : root.recent-tooltip-groups;
private property <bool> online: root.gallery-mode ? gallery.hovered-device.online : root.recent-tooltip-online;
private property <bool> presence-known: !root.gallery-mode || gallery.hovered-device.presence-known;
private property <bool> has-host-name: self.host-name != "" && self.host-name != self.device-name;
private property <color> status-color: self.online ? WorkbenchStyle.success : WorkbenchStyle.muted;
private property <length> label-width: max(tooltip-id-label.preferred-width, tooltip-group-label.preferred-width);
private property <length> pointer-x: root.gallery-mode ? gallery.x + gallery.device-tooltip-x : root.recent-tooltip-pointer-x;
private property <length> pointer-y: root.gallery-mode ? gallery.y + gallery.device-tooltip-y : root.recent-tooltip-pointer-y;
// Follow the pointer with a gap; change sides only when space runs out.
x: clamp(self.pointer-x + self.width + 12px <= parent.width - 8px
? self.pointer-x + 12px : self.pointer-x - self.width - 12px,
8px, max(8px, parent.width - self.width - 8px));
y: clamp(self.pointer-y - self.height - 12px >= 8px
? self.pointer-y - self.height - 12px : self.pointer-y + 16px,
8px, max(8px, parent.height - self.height - 8px));
// Fit the text instead of reserving empty space for short device names.
width: min(220px, parent.width - 16px, max(160px,
tooltip-name.preferred-width + 24px,
self.has-host-name ? tooltip-host.preferred-width + 24px : 0px,
tooltip-name-label.preferred-width + tooltip-status.width + 32px,
self.label-width + max(tooltip-id.preferred-width, tooltip-groups.preferred-width) + 32px));
height: tooltip-groups.y + tooltip-groups.height + 10px;
background: WorkbenchStyle.surface;
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 <length> content-inset: 26px;
private property <length> scrollbar-gutter: 22px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(400px, parent.width - 40px);
height: min(446px, parent.height - 16px);
background: #f8fafc; border-radius: UiRadius.surface; 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 <length> 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 <length> dialog-width: min(480px, self.width - 40px);
private property <length> 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(); }
}
}
}
}
}
}
}