import { UiRadius } from "ui_radius.slint"; import { ComboBox, ScrollView } from "std-widgets.slint"; import { RemoteCursor } from "remote_cursor.slint"; import { FontAwesomeIcons, IconButton, PrimaryButton, SecondaryButton, DialogSurface, ImGuiLineStyle, ImGuiFontStyle } from "common.slint"; export struct StreamTab { remote-id: string, title: string, connected: bool, } export struct FileTransferEntry { name: string, status: string, progress: float, speed: string, size: string, } export struct NetworkStatsRow { label: string, inbound: string, outbound: string, loss-rate: string, } export global StreamStrings { in-out property remote-update-title: "Remote update recommended"; in-out property remote-update: "Update the remote device to the latest version for improved features and performance."; in-out property remote-update-dismiss: "Got it"; in-out property video-settings: "Video settings"; in-out property video-settings-unavailable: "The remote device does not support live video settings"; in-out property video-quality: "Video quality"; in-out property frame-rate: "Capture frame rate"; in-out property adaptation-policy: "Video preference"; in-out property quality-low: "Low"; in-out property quality-medium: "Medium"; in-out property quality-high: "High"; in-out property priority-frame-rate: "Frame rate"; in-out property priority-quality: "Quality"; in-out property priority-balanced: "Balanced"; in-out property select-display: "Select Display"; in-out property send-shortcut: "Send Shortcut"; in-out property privacy-enable: "Enable privacy screen"; in-out property privacy-disable: "Turn privacy off / recover"; in-out property control-mouse: "Control Mouse"; in-out property release-mouse: "Release Mouse"; in-out property audio: "Audio Capture"; in-out property mute: "Mute"; in-out property select-file: "Select File to Send"; in-out property show-stats: "Show Net Traffic Stats"; in-out property hide-stats: "Hide Net Traffic Stats"; in-out property fullscreen: "Fullscreen"; in-out property exit-fullscreen: "Exit fullscreen"; in-out property disconnect: "Disconnect"; in-out property confirm-disconnect: "Disconnect this session?"; in-out property disconnect-description: "The current remote session will end."; in-out property cancel: "Cancel"; in-out property file-transfer: "File Transfer Progress"; in-out property expand-control: "Expand Control Bar"; in-out property collapse-control: "Collapse Control Bar"; in-out property stats-in: "In"; in-out property stats-out: "Out"; in-out property stats-loss-rate: "Loss Rate"; in-out property stats-resolution: "Res"; in-out property stats-video-latency: "Video delay"; in-out property stats-connection-latency: "Connection RTT"; in-out property stats-connection-mode: "Mode"; in-out property stats-encoder: "Remote encoder"; in-out property stats-decoder: "Local decoder"; in-out property stats-encryption: "Encryption"; in-out property stats-encryption-enabled: "Enabled"; in-out property stats-encryption-disabled: "Disabled"; } struct ControlBarTooltip { text: string, anchor-x: length, } // Shared surface treatment for the capsule and its detached detail cards. component StreamControlSurface inherits Rectangle { background: white; border-width: 1px; border-color: #dbe5f1; border-radius: 16px; drop-shadow-color: #1d35571a; drop-shadow-blur: 10px; drop-shadow-offset-y: 2px; } // Keep the compact 16px icons inside roomier circular targets. This remains // local to the stream window; other windows use the larger shared IconButton. component ControlBarButton inherits Rectangle { in property icon; in property show-logo: false; in property logo-pressed: false; in property logo-dragging: false; in property reveal-progress: 1; in property badge: ""; in property tooltip: ""; in property selected: false; in property danger: false; in property slashed: false; in property action-enabled: true; // Capability and temporary command availability have different visuals. in property muted: false; in property icon-size: 16px; out property has-hover: touch.has-hover; out property tooltip-info: { text: root.visible && touch.has-hover && !touch.pressed && !root.logo-pressed ? root.tooltip : "", anchor-x: root.x + root.width / 2, }; callback clicked; callback pointer-input(PointerEventButton, PointerEventKind, length, length); private property button-hovered: !root.show-logo && root.action-enabled && !root.muted && touch.has-hover; private property button-pressed: !root.show-logo && root.action-enabled && !root.muted && touch.pressed; // Share the fill with the slash cutout so every icon stays legible while // transitioning between idle, hover and pressed states. private property button-fill: root.muted ? #eef0f3 : root.button-pressed ? (root.danger ? #7a271a : #65a5f2) : root.button-hovered ? (root.danger ? (root.selected ? #912018 : #b42318) : root.selected ? #86baff : #a7ceff) : root.danger ? (root.selected ? #b42318 : #d92d20) : root.selected ? #b3d5fd : #deecff; private property button-foreground: root.muted ? #8a8f96 : root.danger ? white : #24272d; animate button-fill, button-foreground { duration: 90ms; easing: ease-out; enabled: !root.show-logo; } // Animate the artwork, never its hit area. Use the owning gesture state // so feedback survives pointer capture and clears on cancellation. private property logo-scale: !root.show-logo || root.logo-dragging ? 1 : root.logo-pressed ? 0.94 : touch.has-hover ? 1.06 : 1; animate logo-scale { duration: root.logo-pressed && !root.logo-dragging ? 70ms : 140ms; easing: ease-out; enabled: root.show-logo; } width: 30px; height: 30px; opacity: root.show-logo ? 1 : root.reveal-progress; background: root.show-logo ? transparent : root.button-fill; border-radius: min(self.width, self.height) / 2; Text { visible: !root.show-logo; text: root.icon; color: root.button-foreground; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: root.icon-size; horizontal-alignment: center; vertical-alignment: center; } // Scale the antialiased SVG plate and mark together for subtle feedback. if root.show-logo: Rectangle { width: (min(parent.width, parent.height) - 2px) * root.logo-scale; height: self.width; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; Image { width: parent.width; height: parent.height; source: @image-url("icons/circle-white.svg"); image-fit: contain; } // Keep the mark inside the circular plate without rounded clipping, // which is unsupported by the software renderer. The source app icon // includes a square white plate around this 104 x 76 px mark area. Image { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; // Keep the mark at 24px while giving the white plate more room. width: 24px * root.logo-scale; height: self.width * 76 / 104; source: @image-url("../../../resources/linux/crossdesk_128x128.png"); source-clip-x: 12; source-clip-y: 26; source-clip-width: 104; source-clip-height: 76; image-fit: contain; } } // Draw the selected display number over the center of the monitor glyph. if root.badge != "": Text { // Use the rendered badge size so one- and multi-digit IDs share the // exact horizontal and vertical center of the button. x: (parent.width - self.preferred-width) / 2 + 0.5px; y: (parent.height - self.preferred-height) / 2 - 0.5px; text: root.badge; color: root.button-foreground; font-size: 11px; } // Draw the two offset disabled-state strokes as paths. Rotated rectangles // stay vertical in the software renderer used for fallback and UI capture. if root.slashed: Path { width: 24px; height: 24px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; viewbox-width: 24; viewbox-height: 24; commands: "M 3.5 3.5 L 20.5 20.5"; stroke: root.button-foreground; stroke-width: 2px; } if root.slashed: Path { width: 24px; height: 24px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; viewbox-width: 24; viewbox-height: 24; commands: "M 2 5 L 19 22"; stroke: root.button-fill; stroke-width: 2px; } touch := TouchArea { mouse-cursor: !root.action-enabled ? default : root.show-logo ? (root.logo-pressed ? grabbing : grab) : pointer; scroll-event(event) => { accept } pointer-event(event) => { root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y); } // Keep disabled controls hit-testable so clicks cannot reach the peer. clicked => { if root.action-enabled { root.clicked(); } } } } component VideoSettingRow inherits VerticalLayout { in property label; in property <[string]> options; in property selected; in property enabled; callback choose(int); spacing: 4px; Text { text: root.label; color: #5c626c; font-size: 11px; height: 18px; } HorizontalLayout { spacing: 4px; for option[index] in root.options: Rectangle { height: 27px; background: !root.enabled ? #f1f1f1 : root.selected == index ? #d7eaff : option-touch.has-hover ? #eaf3ff : #f5f6f8; border-radius: UiRadius.control; border-width: 1px; border-color: root.enabled && root.selected == index ? #4296fa : #dce0e6; Text { text: option; color: root.enabled ? #30343b : #8b8f95; font-size: 11px; horizontal-alignment: center; vertical-alignment: center; } option-touch := TouchArea { mouse-cursor: root.enabled ? pointer : default; clicked => { if root.enabled { root.choose(index); } } scroll-event(event) => { accept } } } } } export component StreamWindow inherits Window { title: "CrossDesk"; icon: @image-url("../../../resources/linux/crossdesk_256x256.png"); preferred-width: 1280px; preferred-height: 720px + (root.custom-titlebar ? 32px : 0px); min-width: 640px; min-height: 360px + (root.custom-titlebar ? 32px : 0px); no-frame: root.custom-titlebar; background: root.native-video-enabled ? transparent : root.custom-titlebar ? transparent : black; default-font-size: ImGuiFontStyle.base; in property custom-titlebar: false; in property native-video-enabled: false; // Export the actual input surface in logical pixels. Native rendering and // input mapping must follow the same layout, including title and tab bars. out property video-area-top: content-layer.y + video.y; out property video-area-width: video.width; out property video-area-height: video.height; in property window-active: true; in property window-maximized: false; in property <[StreamTab]> tabs; in-out property selected-tab: 0; in property frame; in property has-frame: false; in property status-text: ""; in property receiving-text: ""; in property remote-update-visible: false; callback dismiss-remote-update; in property <[string]> displays; in-out property selected-display: 0; in property mouse-control-enabled: true; in property privacy-can-toggle: false; in property privacy-active: false; in property privacy-off: false; in property privacy-unsupported: false; // Privacy feedback stays on its toolbar button and hover tooltip. in property privacy-status-text: "Privacy screen"; // Numeric values mirror crossdesk::RemoteCursorShape and Slint's MouseCursor. in property remote-cursor-active: false; in property remote-cursor-shape: 0; in property audio-enabled: true; in property video-settings-enabled: false; in property video-settings-feedback: ""; in-out property video-quality-index: 2; in-out property frame-rate-index: 1; in-out property adaptation-policy-index: 1; callback change-video-settings(int, int, int); in property fullscreen-enabled: false; in property stats-visible: false; in property <[NetworkStatsRow]> stats-rows; in property stats-fps: "0"; in property stats-resolution: ""; in property stats-video-latency: "—"; in property stats-connection-latency: "—"; in property stats-connection-mode: ""; in property stats-encoder: "—"; in property stats-decoder: "—"; in property stats-encryption-enabled: false; in property file-transfer-visible: false; in property <[FileTransferEntry]> file-transfers; callback select-tab(int); callback reorder-tab(int, length, length); callback close-tab(string); callback switch-display(int); in property secure-attention-supported: true; private property <[string]> secure-shortcut-items: ["Ctrl+Alt+Del", "Win+L"]; private property <[string]> ordinary-shortcut-items: ["Win+L"]; private property <[string]> shortcut-items: root.secure-attention-supported ? root.secure-shortcut-items : root.ordinary-shortcut-items; private property shortcut-menu-height: root.shortcut-items.length * 30px + 16px; callback send-shortcut(string); callback toggle-mouse-control; callback toggle-privacy; callback toggle-audio; callback select-file; callback close-file-transfer; pure callback can-drop-file(data-transfer) -> bool; callback file-dropped(data-transfer); callback toggle-stats; callback toggle-fullscreen; callback disconnect; callback stream-title-drag(int, length, length); callback minimize-stream-window; callback toggle-maximize-stream-window; callback close-stream-window; callback pointer-input(PointerEventButton, PointerEventKind, length, length); callback scroll-input(length, length, length, length); callback key-input(string, bool, bool, bool, bool, bool); callback keyboard-focus-changed(bool); private property control-expanded: false; private property control-visual-expanded: false; private property control-toolbar-height: 38px; private property control-button-step: 32px; private property control-disconnect-gap: 8px; private property control-buttons-x: root.control-docked-left ? 43px : 7px; private property control-panel-gap: 8px; private property control-reveal-progress: 0; private property control-surface-progress: min(1, root.control-reveal-progress / 0.65); private property control-content-progress: max(0, (root.control-reveal-progress - 0.65) / 0.35); private property control-content-visible: root.control-visual-expanded && root.control-content-progress > 0; private property control-surface-width: root.control-toolbar-height + (control.width - root.control-toolbar-height) * root.control-surface-progress; private property control-surface-x: control.x + (root.control-docked-left ? 0px : control.width - root.control-surface-width); private property control-collapse-duration: 180ms; private property control-animation-enabled: false; private property stats-row-height: 18px; private property stats-header-height: 18px; private property stats-content-height: root.stats-header-height + root.stats-rows.length * root.stats-row-height + 114px; private property stats-panel-height: min(root.stats-content-height + 16px, max(0px, video.height - root.control-toolbar-height - root.control-panel-gap)); private property stats-panel-y: root.control-panel-y(root.stats-panel-height); private property file-panel-x: 18px; private property file-panel-y: video.height - min(210px, video.height * 0.35); private property file-panel-width: min(430px, video.width * 0.6); private property file-panel-height: min(190px, video.height * 0.32); private property display-menu-open: false; private property display-menu-animation-enabled: false; private property display-menu-progress: 0.0; private property display-menu-height: min(180px, root.displays.length * 30px + 16px); private property display-menu-y: root.control-panel-y(root.display-menu-height); private property shortcut-menu-open: false; private property shortcut-menu-y: root.control-panel-y(root.shortcut-menu-height); in-out property video-menu-open: false; private property video-menu-width: min(320px, video.width - 8px); private property video-menu-height: root.video-settings-enabled && root.video-settings-feedback == "" ? 182px : 220px; private property video-menu-x: min(max(4px, control.x + video-button.x), video.width - root.video-menu-width - 4px); private property video-menu-y: root.control-panel-y(root.video-menu-height); private property disconnect-menu-open: false; private property disconnect-menu-width: min(220px, video.width - 8px); private property disconnect-menu-height: disconnect-actions.y + disconnect-actions.height + 12px; private property disconnect-menu-x: min(max(4px, control.x + close-button.x + close-button.width - root.disconnect-menu-width), video.width - root.disconnect-menu-width - 4px); private property disconnect-menu-y: root.control-panel-y(root.disconnect-menu-height); private property control-menu-open: root.display-menu-open || root.shortcut-menu-open || root.video-menu-open || root.disconnect-menu-open; changed selected-tab => { root.close-control-menus(); root.cancel-remote-pointer(); } changed control-expanded => { control-reveal-timer.running = false; control-collapse-timer.running = false; if root.control-expanded { if !root.control-visual-expanded { root.control-animation-enabled = false; root.control-reveal-progress = 0; root.control-visual-expanded = true; control-reveal-timer.running = true; } else { // Reverse an unfinished collapse from its current frame. root.control-animation-enabled = true; root.control-reveal-progress = 1; } } else { root.close-control-menus(); if root.control-reveal-progress > 0 { root.control-animation-enabled = true; root.control-reveal-progress = 0; control-collapse-timer.running = true; } else { root.finish-control-collapse(); } } } animate control-reveal-progress { duration: root.control-expanded ? 160ms : root.control-collapse-duration; easing: root.control-expanded ? ease-out : ease-in-out; enabled: root.control-animation-enabled; } control-reveal-timer := Timer { interval: 16ms; running: false; triggered => { self.running = false; if root.control-expanded { root.control-animation-enabled = true; root.control-reveal-progress = 1; } } } control-collapse-timer := Timer { // Retain the layout through the final animation frame. interval: root.control-collapse-duration + 16ms; running: false; triggered => { self.running = false; if !root.control-expanded { root.finish-control-collapse(); } } } function finish-control-collapse() { if root.control-dragging { // A drag can begin while the capsule is shrinking. Preserve the // logo's position and pointer offset when its container contracts. let logo-offset = logo-button.x; root.control-drag-x = control.x + logo-offset; root.control-press-x = root.control-press-x - logo-offset; } root.control-animation-enabled = false; root.control-reveal-progress = 0; root.control-visual-expanded = false; } changed window-active => { if !root.window-active { root.close-control-menus(); root.cancel-remote-pointer(); root.control-drag-pending = false; root.end-control-drag(); } } changed mouse-control-enabled => { if !root.mouse-control-enabled { root.cancel-remote-pointer(); } } changed remote-update-visible => { if root.remote-update-visible { root.close-control-menus(); root.control-drag-pending = false; root.end-control-drag(); } // Changed handlers may run while Winit/AccessKit is building the // accessibility tree. Changing focus there re-enters its borrowed // adapter and aborts the process; defer focus to the event loop. input-focus-timer.running = true; input-focus-timer.restart(); root.keyboard-focus-changed(!root.remote-update-visible); } input-focus-timer := Timer { interval: 1ms; running: false; triggered => { self.running = false; input-focus.focus(); } } private property control-tooltip-visible: false; private property hovered-control: { if root.window-active && !root.control-dragging && !root.control-menu-open { if logo-button.has-hover { return logo-button.tooltip-info; } if root.control-expanded && root.control-content-visible { if display-button.has-hover { return display-button.tooltip-info; } if shortcut-button.has-hover { return shortcut-button.tooltip-info; } if mouse-button.has-hover { return mouse-button.tooltip-info; } if privacy-button.has-hover { return privacy-button.tooltip-info; } if video-button.has-hover { return video-button.tooltip-info; } if audio-button.has-hover { return audio-button.tooltip-info; } if file-button.has-hover { return file-button.tooltip-info; } if stats-button.has-hover { return stats-button.tooltip-info; } if fullscreen-button.has-hover { return fullscreen-button.tooltip-info; } if close-button.has-hover { return close-button.tooltip-info; } } } return { text: "", anchor-x: 0px }; } // Own each remote button from down through up/cancel, independent of the // pointer's current position or local overlays appearing during a drag. private property remote-left-down: false; private property remote-right-down: false; private property remote-middle-down: false; private property remote-pointer-active: root.remote-left-down || root.remote-right-down || root.remote-middle-down; private property control-drag-pending: false; private property control-drag-start-x: 0px; private property control-drag-start-y: 0px; private property control-docked-left: true; private property control-dragging: false; private property control-drag-x: 0px; // Start flush against the title/tab strip. This remains mutable so a // manually dragged control bar keeps its chosen vertical position. private property control-pos-y: 0px; private property control-press-x: 0px; private property control-press-y: 0px; private property dragging-tab: -1; private property titlebar-height: root.custom-titlebar && !root.fullscreen-enabled ? 32px : 0px; private property window-corner-radius: 12px; callback begin-control-drag(length, length); callback move-control-drag(length, length); callback end-control-drag; pure callback is-local-control-area(length, length) -> bool; pure function control-panel-y(panel-height: length) -> length { let below = control.y + root.control-toolbar-height + root.control-panel-gap; return below + panel-height <= video.height ? below : max(0px, control.y - root.control-panel-gap - panel-height); } function close-control-menus() { display-menu-open-timer.running = false; root.display-menu-animation-enabled = false; root.display-menu-progress = 0.0; root.display-menu-open = false; root.shortcut-menu-open = false; root.video-menu-open = false; root.disconnect-menu-open = false; } // Native focus/session changes release input in C++; clear the matching UI // ownership too, including when replacing the only tab (same tab index). public function reset-input-state() { root.remote-left-down = false; root.remote-right-down = false; root.remote-middle-down = false; root.close-control-menus(); root.control-drag-pending = false; root.end-control-drag(); } function cancel-remote-pointer() { if root.remote-pointer-active { root.reset-input-state(); root.pointer-input(PointerEventButton.other, PointerEventKind.cancel, pointer.mouse-x, pointer.mouse-y); } } pure function remote-button-down(button: PointerEventButton) -> bool { return button == PointerEventButton.left ? root.remote-left-down : button == PointerEventButton.right ? root.remote-right-down : button == PointerEventButton.middle ? root.remote-middle-down : false; } function set-remote-button(button: PointerEventButton, down: bool) { if button == PointerEventButton.left { root.remote-left-down = down; } if button == PointerEventButton.right { root.remote-right-down = down; } if button == PointerEventButton.middle { root.remote-middle-down = down; } } function route-remote-pointer(button: PointerEventButton, kind: PointerEventKind, x: length, y: length) { // Releases and cancellation must not be gated on hit testing. A local // popup removing itself on click cannot manufacture a remote release. if kind == PointerEventKind.cancel { root.cancel-remote-pointer(); } else if kind == PointerEventKind.up { if root.remote-button-down(button) { root.set-remote-button(button, false); root.pointer-input(button, kind, x, y); } } else if root.window-active && root.mouse-control-enabled && !root.remote-update-visible { let remote-target = root.remote-pointer-active || (!root.control-menu-open && !root.is-local-control-area(x, y)); if kind == PointerEventKind.down && remote-target && button != PointerEventButton.other { input-focus.focus(); root.set-remote-button(button, true); root.pointer-input(button, kind, x, y); } else if kind == PointerEventKind.move && remote-target { root.pointer-input(button, kind, x, y); } } } function route-remote-scroll(dx: length, dy: length, x: length, y: length) { if root.window-active && root.mouse-control-enabled && !root.control-menu-open && !root.is-local-control-area(x, y) { root.scroll-input(dx, dy, x, y); } } function arm-control-drag(x: length, y: length) { root.close-control-menus(); root.control-drag-start-x = x; root.control-drag-start-y = y; root.control-drag-pending = true; } function update-control-drag(button: PointerEventButton, kind: PointerEventKind, x: length, y: length) { if kind == PointerEventKind.cancel || (kind == PointerEventKind.up && button == PointerEventButton.left) { // Slint emits clicked before pointer-up. Resolve logo clicks here // so a captured drag can never also toggle the toolbar. let toggle-expanded = kind == PointerEventKind.up && root.control-drag-pending && !root.control-dragging && root.window-active && !root.remote-update-visible && x >= control.x + logo-button.x && x < control.x + logo-button.x + logo-button.width && y >= control.y + logo-button.y && y < control.y + logo-button.y + logo-button.height; root.control-drag-pending = false; root.end-control-drag(); if toggle-expanded { root.control-expanded = !root.control-expanded; } } else if kind == PointerEventKind.move && root.control-drag-pending { if !root.control-dragging && (abs(x - root.control-drag-start-x) >= 5px || abs(y - root.control-drag-start-y) >= 5px) { root.begin-control-drag(root.control-drag-start-x, root.control-drag-start-y); } root.move-control-drag(x, y); } } animate display-menu-progress { duration: 120ms; easing: ease-out; enabled: root.display-menu-animation-enabled; } display-menu-open-timer := Timer { interval: 16ms; running: false; triggered => { self.running = false; if root.display-menu-open { root.display-menu-animation-enabled = true; root.display-menu-progress = 1.0; } } } changed hovered-control => { root.control-tooltip-visible = false; control-tooltip-timer.running = root.hovered-control.text != ""; if root.hovered-control.text != "" { control-tooltip-timer.restart(); } } control-tooltip-timer := Timer { interval: 450ms; running: false; triggered => { self.running = false; root.control-tooltip-visible = true; } } is-local-control-area(pointer-x, pointer-y) => { if root.remote-update-visible { return true; } let over-control = pointer-x >= root.control-surface-x && pointer-x <= root.control-surface-x + root.control-surface-width && pointer-y >= control.y && pointer-y <= control.y + control.height; let over-stats = root.stats-visible && root.control-content-visible && pointer-x >= control.x && pointer-x <= control.x + control.width && pointer-y >= root.stats-panel-y && pointer-y <= root.stats-panel-y + root.stats-panel-height; let display-menu-x = control.x + display-button.x; let over-display-menu = root.display-menu-open && pointer-x >= display-menu-x && pointer-x <= display-menu-x + 180px && pointer-y >= root.display-menu-y && pointer-y <= root.display-menu-y + root.display-menu-height; let shortcut-menu-x = control.x + shortcut-button.x; let over-shortcut-menu = root.shortcut-menu-open && pointer-x >= shortcut-menu-x && pointer-x <= shortcut-menu-x + 180px && pointer-y >= root.shortcut-menu-y && pointer-y <= root.shortcut-menu-y + root.shortcut-menu-height; let over-video-menu = root.video-menu-open && pointer-x >= root.video-menu-x && pointer-x <= root.video-menu-x + root.video-menu-width && pointer-y >= root.video-menu-y && pointer-y <= root.video-menu-y + root.video-menu-height; let over-disconnect-menu = root.disconnect-menu-open && pointer-x >= root.disconnect-menu-x && pointer-x <= root.disconnect-menu-x + root.disconnect-menu-width && pointer-y >= root.disconnect-menu-y && pointer-y <= root.disconnect-menu-y + root.disconnect-menu-height; let over-file-panel = root.file-transfer-visible && pointer-x >= root.file-panel-x && pointer-x <= root.file-panel-x + root.file-panel-width && pointer-y >= root.file-panel-y && pointer-y <= root.file-panel-y + root.file-panel-height; return over-control || over-stats || over-display-menu || over-shortcut-menu || over-video-menu || over-disconnect-menu || over-file-panel; } begin-control-drag(press-x, press-y) => { // Capture the snapped position before switching x to control-drag-x. // Otherwise a second press briefly restores the previous drag target // and makes a plain click move the control bar. let current-x = control.x; let current-y = control.y; root.control-drag-x = current-x; // Store the pointer offset in the control bar, while all following // pointer positions remain relative to the stationary video surface. root.control-press-x = press-x - current-x; root.control-press-y = press-y - current-y; root.close-control-menus(); root.control-dragging = true; } move-control-drag(pointer-x, pointer-y) => { if root.control-dragging { root.control-drag-x = min(max(0px, pointer-x - root.control-press-x), video.width - control.width); root.control-pos-y = min(max(0px, pointer-y - root.control-press-y), video.height - control.height); } } end-control-drag() => { if root.control-dragging { root.control-docked-left = root.control-drag-x + control.width / 2 < video.width / 2; root.control-pos-y = min(max(0px, control.y), video.height - control.height); root.control-dragging = false; } } window-surface := Rectangle { width: root.width; height: root.height; background: root.native-video-enabled ? transparent : black; border-radius: root.custom-titlebar && !root.fullscreen-enabled && !root.window-maximized ? root.window-corner-radius : 0px; clip: root.custom-titlebar && !root.fullscreen-enabled; if root.custom-titlebar && !root.fullscreen-enabled: Rectangle { x: 0px; y: 0px; width: parent.width; height: root.titlebar-height; background: #f6f5f4; z: 100; Text { x: 120px; width: parent.width - 240px; height: parent.height; text: "CrossDesk"; color: root.window-active ? #3d3d3d : #747474; font-size: 12px; font-weight: 600; horizontal-alignment: center; vertical-alignment: center; overflow: elide; } stream-title-drag-area := TouchArea { width: parent.width - 120px; property phase: self.pressed ? 1 : 0; changed phase => { root.stream-title-drag(phase, self.mouse-x, self.mouse-y); } moved => { if (self.pressed) { root.stream-title-drag(2, self.mouse-x, self.mouse-y); } } double-clicked => { root.toggle-maximize-stream-window(); } } stream-minimize-button := Rectangle { x: parent.width - 114px; y: 4px; width: 24px; height: 24px; border-radius: 12px; background: stream-minimize-touch.pressed ? #c0bfbc : stream-minimize-touch.has-hover ? #deddda : root.window-active ? #e6e6e6 : transparent; accessible-role: button; accessible-label: "Minimize"; accessible-action-default => { stream-minimize-touch.clicked(); } Rectangle { x: 8px; y: 13px; width: 8px; height: 0.75px; background: root.window-active || stream-minimize-touch.has-hover || stream-minimize-touch.pressed ? #454545 : #898989; } stream-minimize-touch := TouchArea { clicked => { root.minimize-stream-window(); } } } stream-maximize-button := Rectangle { x: parent.width - 74px; y: 4px; width: 24px; height: 24px; border-radius: 12px; background: stream-maximize-touch.pressed ? #c0bfbc : stream-maximize-touch.has-hover ? #deddda : root.window-active ? #e6e6e6 : transparent; accessible-role: button; accessible-label: root.window-maximized ? "Restore" : "Maximize"; accessible-action-default => { stream-maximize-touch.clicked(); } if !root.window-maximized: Rectangle { x: 8px; y: 8px; width: 8px; height: 8px; background: transparent; border-width: 1px; border-color: root.window-active || stream-maximize-touch.has-hover || stream-maximize-touch.pressed ? #454545 : #898989; } if root.window-maximized: Rectangle { x: 9px; y: 7px; width: 8px; height: 8px; background: transparent; border-width: 1px; border-color: root.window-active || stream-maximize-touch.has-hover || stream-maximize-touch.pressed ? #454545 : #898989; } if root.window-maximized: Rectangle { x: 7px; y: 9px; width: 8px; height: 8px; background: stream-maximize-button.background; border-width: 1px; border-color: root.window-active || stream-maximize-touch.has-hover || stream-maximize-touch.pressed ? #454545 : #898989; } stream-maximize-touch := TouchArea { clicked => { root.toggle-maximize-stream-window(); } } } stream-close-button := Rectangle { x: parent.width - 34px; y: 4px; width: 24px; height: 24px; border-radius: 12px; background: stream-close-touch.pressed ? #b2161d : stream-close-touch.has-hover ? #e01b24 : root.window-active ? #e6e6e6 : transparent; accessible-role: button; accessible-label: "Close"; accessible-action-default => { stream-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: stream-close-touch.has-hover || stream-close-touch.pressed ? white : root.window-active ? #454545 : #898989; stroke-width: 1px; } stream-close-touch := TouchArea { clicked => { root.close-stream-window(); } } } Rectangle { y: parent.height - 1px; height: 1px; background: #d5d3d0; } } content-layer := Rectangle { y: root.titlebar-height; width: parent.width; height: parent.height - self.y; background: root.native-video-enabled ? transparent : black; clip: true; // Keep the ImGui tab strip behavior, including pointer-drag reordering. Rectangle { y: 0px; height: root.tabs.length > 1 ? 30px : 0px; visible: !root.fullscreen-enabled && root.tabs.length > 1; background: #f1f2f4; z: 10; HorizontalLayout { spacing: 1px; for tab[index] in root.tabs: tab-item := Rectangle { width: min(190px, max(110px, (parent.width - 12px) / root.tabs.length)); background: index == root.selected-tab ? white : tab-touch.has-hover || root.dragging-tab == index ? #e5e7eb : #d8dbe0; border-width: root.dragging-tab == index ? 1px : 0px; border-color: #2463c7; Rectangle { width: parent.width - 27px; height: parent.height; background: transparent; Text { x: 0px; width: parent.width - self.x; height: parent.height; text: tab.title; color: #30343b; font-size: ImGuiFontStyle.base; overflow: elide; vertical-alignment: center; } } Rectangle { y: parent.height - 2px; height: 2px; background: index == root.selected-tab ? #3b82f6 : transparent; } tab-touch := TouchArea { width: parent.width - 27px; changed pressed => { if (self.pressed) { root.dragging-tab = index; } else if (root.dragging-tab == index) { root.reorder-tab(index, tab-item.x + self.mouse-x, tab-item.width); root.dragging-tab = -1; } } clicked => { root.selected-tab = index; root.select-tab(index); } } IconButton { x: parent.width - 27px; y: 1px; width: 27px; height: 27px; z: 2; icon: FontAwesomeIcons.xmark; icon-size: 11px; icon-color: #555c66; danger: true; clicked => { root.close-tab(tab.remote-id); } } } } } video := Rectangle { y: root.fullscreen-enabled ? 0px : (root.tabs.length > 1 ? 30px : 0px); height: parent.height - self.y; background: root.native-video-enabled ? transparent : black; clip: true; frame-image := Image { width: parent.width; height: parent.height; source: root.frame; image-fit: contain; visible: root.has-frame && !root.native-video-enabled; } Text { // A connection state and the frame-waiting state may change in // the same UI tick. Render them through one text item so two // independent labels can never occupy the center together. visible: !root.has-frame && (root.receiving-text != "" || root.status-text != ""); text: root.receiving-text != "" ? root.receiving-text : root.status-text; color: root.receiving-text != "" ? #ffffffea : white; font-size: root.receiving-text != "" ? 14px : ImGuiFontStyle.prominent; horizontal-alignment: center; vertical-alignment: center; } drop-zone := DropArea { can-drop(event) => { return !root.remote-update-visible && root.can-drop-file(event.data) ? DragAction.copy : DragAction.none; } dropped(event) => { if root.remote-update-visible { return DragAction.none; } root.file-dropped(event.data); return DragAction.copy; } } input-focus := FocusScope { focus-on-click: true; focus-gained => { root.keyboard-focus-changed(true); } focus-lost => { root.keyboard-focus-changed(false); } key-pressed(event) => { if !root.remote-update-visible { root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta); } accept } key-released(event) => { if root.remote-update-visible { // Dismiss on release so the same key cannot reach the peer. if event.text == Key.Escape || event.text == Key.Return { root.dismiss-remote-update(); } } else { root.key-input(event.text, false, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta); } accept } pointer := TouchArea { mouse-cursor: root.remote-cursor-active ? RemoteCursor.shape(root.remote-cursor-shape) : MouseCursor.default; pointer-event(event) => { root.route-remote-pointer(event.button, event.kind, self.mouse-x, self.mouse-y); } scroll-event(event) => { root.route-remote-scroll(event.delta-x, event.delta-y, self.mouse-x, self.mouse-y); accept } } } // Slint drops its implicit grab on any button-up. Keep the same // owner above local UI until every remote button has been released. // The logo also retains a stable owner if another button is used. if root.remote-pointer-active || root.control-drag-pending: TouchArea { z: 15; mouse-cursor: root.control-drag-pending ? MouseCursor.grabbing : root.remote-cursor-active ? RemoteCursor.shape(root.remote-cursor-shape) : MouseCursor.default; pointer-event(event) => { if root.control-drag-pending { root.update-control-drag(event.button, event.kind, self.mouse-x, self.mouse-y); } else { root.route-remote-pointer(event.button, event.kind, self.mouse-x, self.mouse-y); } } scroll-event(event) => { if !root.control-drag-pending { root.route-remote-scroll(event.delta-x, event.delta-y, self.mouse-x, self.mouse-y); } accept } } // Keep the toolbar anchor and hit area stable while the visual // capsule grows from the logo's docked edge. control := Rectangle { x: root.control-dragging ? root.control-drag-x : root.control-docked-left ? 0px : parent.width - self.width; y: min(max(0px, root.control-pos-y), parent.height - self.height); width: root.control-visual-expanded ? min(80px + 9 * root.control-button-step + root.control-disconnect-gap, parent.width) : root.control-toolbar-height; height: root.control-toolbar-height; z: 10; if root.control-visual-expanded: StreamControlSurface { width: root.control-surface-width; height: root.control-toolbar-height; x: root.control-docked-left ? 0px : parent.width - self.width; border-radius: root.control-toolbar-height / 2; opacity: min(1, root.control-reveal-progress / 0.15); } // Padding stays local; only the logo starts dragging. TouchArea { x: root.control-surface-x - control.x; width: root.control-surface-width; mouse-cursor: default; clicked => { root.close-control-menus(); } scroll-event(event) => { accept } } display-button := ControlBarButton { reveal-progress: root.control-content-progress; visible: root.control-content-visible; action-enabled: root.control-expanded; x: root.control-buttons-x; y: (parent.height - self.height) / 2; icon: FontAwesomeIcons.display; badge: root.selected-display + 1; tooltip: StreamStrings.select-display; clicked => { display-menu-open-timer.running = false; root.display-menu-animation-enabled = false; root.display-menu-progress = 0.0; root.display-menu-open = !root.display-menu-open; root.shortcut-menu-open = false; root.video-menu-open = false; root.disconnect-menu-open = false; if root.display-menu-open { display-menu-open-timer.running = true; } } } shortcut-button := ControlBarButton { reveal-progress: root.control-content-progress; visible: root.control-content-visible; action-enabled: root.control-expanded; x: root.control-buttons-x + root.control-button-step; y: (parent.height - self.height) / 2; icon: FontAwesomeIcons.keyboard; tooltip: StreamStrings.send-shortcut; clicked => { display-menu-open-timer.running = false; root.display-menu-animation-enabled = false; root.display-menu-progress = 0.0; root.shortcut-menu-open = !root.shortcut-menu-open; root.display-menu-open = false; root.video-menu-open = false; root.disconnect-menu-open = false; } } mouse-button := ControlBarButton { reveal-progress: root.control-content-progress; visible: root.control-content-visible; action-enabled: root.control-expanded; x: root.control-buttons-x + 2 * root.control-button-step; y: (parent.height - self.height) / 2; icon: FontAwesomeIcons.computer-mouse; slashed: !root.mouse-control-enabled; tooltip: root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse; clicked => { root.close-control-menus(); root.toggle-mouse-control(); } } audio-button := ControlBarButton { reveal-progress: root.control-content-progress; visible: root.control-content-visible; action-enabled: root.control-expanded; x: root.control-buttons-x + 4 * root.control-button-step; y: (parent.height - self.height) / 2; icon: FontAwesomeIcons.volume-high; slashed: !root.audio-enabled; tooltip: root.audio-enabled ? StreamStrings.mute : StreamStrings.audio; clicked => { root.close-control-menus(); root.toggle-audio(); } } file-button := ControlBarButton { reveal-progress: root.control-content-progress; visible: root.control-content-visible; action-enabled: root.control-expanded; x: root.control-buttons-x + 5 * root.control-button-step; y: (parent.height - self.height) / 2; icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file; clicked => { root.close-control-menus(); root.select-file(); } } video-button := ControlBarButton { reveal-progress: root.control-content-progress; visible: root.control-content-visible; action-enabled: root.control-expanded; x: root.control-buttons-x + 6 * root.control-button-step; y: (parent.height - self.height) / 2; icon: "\u{f1de}"; selected: root.video-menu-open; tooltip: StreamStrings.video-settings; clicked => { root.video-menu-open = !root.video-menu-open; root.display-menu-open = false; root.shortcut-menu-open = false; root.disconnect-menu-open = false; } } stats-button := ControlBarButton { reveal-progress: root.control-content-progress; visible: root.control-content-visible; action-enabled: root.control-expanded; x: root.control-buttons-x + 7 * root.control-button-step; y: (parent.height - self.height) / 2; icon: FontAwesomeIcons.signal; selected: root.stats-visible; tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats; clicked => { root.close-control-menus(); root.toggle-stats(); } } fullscreen-button := ControlBarButton { reveal-progress: root.control-content-progress; visible: root.control-content-visible; action-enabled: root.control-expanded; x: root.control-buttons-x + 8 * root.control-button-step; y: (parent.height - self.height) / 2; icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand; tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen; clicked => { root.close-control-menus(); root.toggle-fullscreen(); } } if root.control-content-visible: Rectangle { opacity: root.control-content-progress; x: close-button.x - 6px; y: 12px; width: 1px; height: 14px; background: #dbe5f1; } close-button := ControlBarButton { reveal-progress: root.control-content-progress; visible: root.control-content-visible; action-enabled: root.control-expanded; x: root.control-buttons-x + 9 * root.control-button-step + root.control-disconnect-gap; y: (parent.height - self.height) / 2; icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect; danger: true; selected: root.disconnect-menu-open; clicked => { let open = !root.disconnect-menu-open; root.close-control-menus(); root.disconnect-menu-open = open; } } logo-button := ControlBarButton { // Stay under the pointer across expansion and collapse. x: root.control-docked-left ? 0px : parent.width - self.width; y: 0px; width: root.control-toolbar-height; height: root.control-toolbar-height; show-logo: true; logo-pressed: root.control-drag-pending; logo-dragging: self.logo-pressed && root.control-dragging; tooltip: root.control-expanded ? StreamStrings.collapse-control : StreamStrings.expand-control; pointer-input(button, kind, pointer-x, pointer-y) => { let x = control.x + logo-button.x + pointer-x; let y = control.y + logo-button.y + pointer-y; if kind == PointerEventKind.down && button == PointerEventButton.left { root.arm-control-drag(x, y); } else { root.update-control-drag(button, kind, x, y); } } } privacy-button := ControlBarButton { reveal-progress: root.control-content-progress; visible: root.control-content-visible; x: root.control-buttons-x + 3 * root.control-button-step; y: (parent.height - self.height) / 2; icon: FontAwesomeIcons.user-secret; slashed: root.privacy-off && !root.privacy-unsupported; muted: root.privacy-unsupported; action-enabled: root.control-expanded && root.privacy-can-toggle; tooltip: root.privacy-status-text + (root.privacy-can-toggle ? "\n" + (root.privacy-active ? StreamStrings.privacy-disable : StreamStrings.privacy-enable) : ""); clicked => { root.close-control-menus(); root.toggle-privacy(); } } } if root.stats-visible && root.control-content-visible: stats-panel := StreamControlSurface { x: control.x; y: root.stats-panel-y; width: control.width; height: root.stats-panel-height; opacity: root.control-content-progress; z: 10; clip: true; TouchArea { mouse-cursor: default; clicked => { root.close-control-menus(); } scroll-event(event) => { accept } } stats-table := Rectangle { x: 8px; y: 8px; width: parent.width - 16px; height: root.stats-content-height; background: transparent; private property row-height: root.stats-row-height; private property label-column-width: 50px; private property loss-column-width: max(56px, stats-loss-label.preferred-width + 12px); private property value-column-width: (self.width - self.label-column-width - self.loss-column-width) / 2; private property details-right-width: right-label-width-probe.preferred-width + 6px + max(resolution-width-probe.preferred-width, connection-latency-width-probe.preferred-width, encryption-width-probe.preferred-width + 12px + (root.stats-encryption-enabled ? 14px : 0px)); private property details-right-x: min(self.width / 2 + 4px, self.width - self.details-right-width - 8px); left-label-width-probe := Text { visible: false; text: "FPS\n" + StreamStrings.stats-video-latency + "\n" + StreamStrings.stats-connection-mode; font-size: ImGuiFontStyle.small; } right-label-width-probe := Text { visible: false; text: StreamStrings.stats-resolution + "\n" + StreamStrings.stats-connection-latency + "\n" + StreamStrings.stats-encryption; font-size: ImGuiFontStyle.small; } resolution-width-probe := Text { visible: false; text: root.stats-resolution; font-size: ImGuiFontStyle.body; font-weight: 600; } connection-latency-width-probe := Text { visible: false; text: root.stats-connection-latency; font-size: ImGuiFontStyle.body; font-weight: 600; } encryption-width-probe := Text { visible: false; text: root.stats-encryption-enabled ? StreamStrings.stats-encryption-enabled : StreamStrings.stats-encryption-disabled; font-size: ImGuiFontStyle.small; font-weight: 600; } Rectangle { x: 0px; y: 0px; width: parent.width; height: root.stats-header-height; background: #f5f7fa; border-radius: UiRadius.control; Text { x: stats-table.label-column-width + 4px; width: stats-table.value-column-width - 8px; text: StreamStrings.stats-in; color: #667085; font-size: ImGuiFontStyle.small; horizontal-alignment: right; vertical-alignment: center; } Text { x: stats-table.label-column-width + stats-table.value-column-width + 4px; width: stats-table.value-column-width - 8px; text: StreamStrings.stats-out; color: #667085; font-size: ImGuiFontStyle.small; horizontal-alignment: right; vertical-alignment: center; } stats-loss-label := Text { x: stats-table.label-column-width + stats-table.value-column-width * 2 + 4px; width: stats-table.loss-column-width - 8px; text: StreamStrings.stats-loss-rate; color: #667085; font-size: ImGuiFontStyle.small; horizontal-alignment: right; vertical-alignment: center; overflow: elide; } } for row[index] in root.stats-rows: Rectangle { x: 0px; y: root.stats-header-height + index * stats-table.row-height; width: parent.width; height: stats-table.row-height; background: index == root.stats-rows.length - 1 ? #edf4ff : transparent; border-radius: UiRadius.control; Text { x: 4px; width: stats-table.label-column-width - 8px; text: row.label; color: index == root.stats-rows.length - 1 ? #275188 : #667085; font-size: ImGuiFontStyle.body; font-weight: index == root.stats-rows.length - 1 ? 600 : 400; vertical-alignment: center; } Text { x: stats-table.label-column-width + 4px; width: stats-table.value-column-width - 8px; text: row.inbound; color: index == root.stats-rows.length - 1 ? #275188 : #344054; font-size: ImGuiFontStyle.body; font-weight: index == root.stats-rows.length - 1 ? 600 : 400; horizontal-alignment: right; vertical-alignment: center; overflow: elide; } Text { x: stats-table.label-column-width + stats-table.value-column-width + 4px; width: stats-table.value-column-width - 8px; text: row.outbound; color: index == root.stats-rows.length - 1 ? #275188 : #344054; font-size: ImGuiFontStyle.body; font-weight: index == root.stats-rows.length - 1 ? 600 : 400; horizontal-alignment: right; vertical-alignment: center; overflow: elide; } Text { x: stats-table.label-column-width + stats-table.value-column-width * 2 + 4px; width: stats-table.loss-column-width - 8px; text: row.loss-rate; color: index == root.stats-rows.length - 1 ? #275188 : #667085; font-size: ImGuiFontStyle.body; font-weight: index == root.stats-rows.length - 1 ? 600 : 400; horizontal-alignment: right; vertical-alignment: center; overflow: elide; } if index < root.stats-rows.length - 2: Rectangle { x: 4px; y: parent.height - 1px; width: parent.width - 8px; height: 1px; background: #edf0f4; } } codec-label-width-probe := Text { visible: false; text: StreamStrings.stats-encoder + "\n" + StreamStrings.stats-decoder; font-size: ImGuiFontStyle.small; } Rectangle { x: 0px; y: root.stats-header-height + root.stats-rows.length * stats-table.row-height + 70px; width: parent.width; height: 44px; background: #f7f9fc; border-radius: UiRadius.control; border-width: 1px; border-color: #e8edf4; Rectangle { x: 8px; y: 22px; width: parent.width - 16px; height: 1px; background: #e8edf4; } for codec[index] in [ { label: StreamStrings.stats-encoder, value: root.stats-encoder }, { label: StreamStrings.stats-decoder, value: root.stats-decoder }, ]: Rectangle { x: 8px; y: 2px + index * 21px; width: parent.width - 16px; height: 19px; Text { x: 0px; y: 0px; width: codec-label-width-probe.preferred-width; height: parent.height; text: codec.label; color: #667085; font-size: ImGuiFontStyle.small; vertical-alignment: center; } Text { x: codec-label-width-probe.preferred-width + 8px; y: 0px; width: parent.width - self.x; height: parent.height; text: codec.value; color: #344054; font-size: ImGuiFontStyle.body; font-weight: 500; horizontal-alignment: right; vertical-alignment: center; overflow: elide; } } } for metric[index] in [ { label: "FPS", value: root.stats-fps }, { label: StreamStrings.stats-resolution, value: root.stats-resolution }, { label: StreamStrings.stats-video-latency, value: root.stats-video-latency }, { label: StreamStrings.stats-connection-latency, value: root.stats-connection-latency }, { label: StreamStrings.stats-connection-mode, value: root.stats-connection-mode }, { label: StreamStrings.stats-encryption, value: root.stats-encryption-enabled ? StreamStrings.stats-encryption-enabled : StreamStrings.stats-encryption-disabled }, ]: Rectangle { private property encryption-icon-width: index == 5 && root.stats-encryption-enabled ? 14px : 0px; x: mod(index, 2) == 1 ? stats-table.details-right-x : 8px; y: root.stats-header-height + root.stats-rows.length * stats-table.row-height + 4px + floor(index / 2) * 20px; width: mod(index, 2) == 1 ? stats-table.width - stats-table.details-right-x - 8px : stats-table.details-right-x - 18px; height: 20px; metric-label := Text { x: 0px; y: 0px; width: mod(index, 2) == 1 ? right-label-width-probe.preferred-width : left-label-width-probe.preferred-width; height: parent.height; text: metric.label; color: #667085; font-size: ImGuiFontStyle.small; vertical-alignment: center; } value-width-probe := Text { visible: false; text: metric.value; font-size: index >= 4 ? ImGuiFontStyle.small : ImGuiFontStyle.body; font-weight: 600; } metric-value := Rectangle { private property badge-padding: index >= 4 ? 6px : 0px; private property icon-width: parent.encryption-icon-width; width: index >= 4 ? min(max(0px, parent.width - metric-label.width - 6px), value-width-probe.preferred-width + self.badge-padding * 2 + self.icon-width) : max(0px, parent.width - metric-label.width - 6px); x: parent.width - self.width; y: 2px; height: 16px; border-radius: 4px; background: index == 4 ? #edf3fc : index == 5 ? (root.stats-encryption-enabled ? #eaf6ee : #fff3e3) : transparent; Text { x: parent.badge-padding; y: 0px; height: parent.height; width: max(0px, parent.width - parent.badge-padding * 2 - parent.icon-width); text: metric.value; color: index == 4 ? #275188 : index == 5 ? (root.stats-encryption-enabled ? #18794e : #946015) : #253247; font-size: index >= 4 ? ImGuiFontStyle.small : ImGuiFontStyle.body; font-weight: 600; horizontal-alignment: right; vertical-alignment: center; overflow: elide; } if index == 5 && root.stats-encryption-enabled: Text { x: parent.width - parent.badge-padding - 10px; y: 2px; width: 10px; height: 12px; text: FontAwesomeIcons.shield-halved; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 10px; color: #238653; horizontal-alignment: center; vertical-alignment: center; } } } } } // Dismiss outside gestures on release, keeping the entire click // local. Toolbar controls remain above this shared menu backdrop. if root.control-menu-open: Rectangle { width: parent.width; height: parent.height; z: 9; TouchArea { mouse-cursor: default; pointer-event(event) => { if event.kind == PointerEventKind.up { root.close-control-menus(); } } scroll-event(event) => { accept } } } disconnect-panel := StreamControlSurface { visible: root.disconnect-menu-open; x: root.disconnect-menu-x; y: root.disconnect-menu-y; width: root.disconnect-menu-width; height: root.disconnect-menu-height; z: 11; TouchArea { mouse-cursor: default; scroll-event(event) => { accept } } disconnect-title := Text { x: 12px; y: 12px; width: parent.width - 24px; height: max(20px, self.preferred-height); text: StreamStrings.confirm-disconnect; color: #30343b; font-size: 14px; font-weight: 600; vertical-alignment: center; wrap: word-wrap; } disconnect-message := Text { x: 12px; y: disconnect-title.y + disconnect-title.height + 4px; width: parent.width - 24px; height: max(18px, self.preferred-height); text: StreamStrings.disconnect-description; color: #667085; font-size: 12px; wrap: word-wrap; } disconnect-actions := HorizontalLayout { x: parent.width - self.width - 12px; y: disconnect-message.y + disconnect-message.height + 12px; width: 168px; height: 28px; spacing: 8px; SecondaryButton { width: 72px; height: 28px; text: StreamStrings.cancel; clicked => { root.close-control-menus(); } } Rectangle { width: 88px; background: confirm-disconnect-touch.pressed ? #912018 : confirm-disconnect-touch.has-hover ? #b42318 : #d92d20; border-radius: UiRadius.control; Text { text: StreamStrings.disconnect; color: white; font-size: ImGuiFontStyle.body; horizontal-alignment: center; vertical-alignment: center; } confirm-disconnect-touch := TouchArea { mouse-cursor: MouseCursor.pointer; scroll-event(event) => { accept } clicked => { root.close-control-menus(); root.disconnect(); } } } } } if root.video-menu-open: StreamControlSurface { x: root.video-menu-x; y: root.video-menu-y; width: root.video-menu-width; height: root.video-menu-height; z: 11; TouchArea { mouse-cursor: default; scroll-event(event) => { accept } } VerticalLayout { padding: 10px; spacing: 8px; VideoSettingRow { label: StreamStrings.video-quality; options: [StreamStrings.quality-low, StreamStrings.quality-medium, StreamStrings.quality-high]; selected: root.video-quality-index; enabled: root.video-settings-enabled; choose(index) => { root.change-video-settings(index, root.frame-rate-index, root.adaptation-policy-index); } } VideoSettingRow { label: StreamStrings.frame-rate; options: ["30 fps", "60 fps"]; selected: root.frame-rate-index; enabled: root.video-settings-enabled; choose(index) => { root.change-video-settings(root.video-quality-index, index, root.adaptation-policy-index); } } VideoSettingRow { label: StreamStrings.adaptation-policy; options: [StreamStrings.priority-frame-rate, StreamStrings.priority-quality, StreamStrings.priority-balanced]; selected: root.adaptation-policy-index; enabled: root.video-settings-enabled; choose(index) => { root.change-video-settings(root.video-quality-index, root.frame-rate-index, index); } } if !root.video-settings-enabled || root.video-settings-feedback != "": Text { text: root.video-settings-enabled ? root.video-settings-feedback : StreamStrings.video-settings-unavailable; wrap: word-wrap; color: #8b8f95; font-size: 11px; } } } if root.display-menu-open: display-menu := StreamControlSurface { private property expanded-height: root.display-menu-height; private property hovered-index: display-touch.has-hover && display-touch.mouse-y >= 8px && floor((display-touch.mouse-y - 8px) / 30px) < root.displays.length ? floor((display-touch.mouse-y - 8px) / 30px) : -1; x: control.x + display-button.x; y: root.display-menu-y; width: 180px; height: self.expanded-height * root.display-menu-progress; opacity: root.display-menu-progress; clip: true; z: 11; VerticalLayout { padding: 8px; spacing: 1px; for display[index] in root.displays: Rectangle { height: 29px; background: index == display-menu.hovered-index && display-touch.pressed ? #bfd4f2 : index == display-menu.hovered-index ? #d8e6f8 : index == root.selected-display ? #e8eef9 : transparent; border-radius: UiRadius.control; Text { x: 8px; width: parent.width - 34px; text: display; color: #30343b; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } if index == root.selected-display: Text { x: parent.width - 24px; width: 16px; text: FontAwesomeIcons.check; color: #2463c7; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 10px; horizontal-alignment: center; vertical-alignment: center; } } } // Use one hit target for the whole popup. A single owner keeps // hover stable while still consuming padding, gaps and scroll // events locally instead of forwarding them to the peer. display-touch := TouchArea { mouse-cursor: default; clicked => { let index = floor((self.mouse-y - 8px) / 30px); let row-y = self.mouse-y - 8px - index * 30px; if self.mouse-y >= 8px && row-y < 29px && index >= 0 && index < root.displays.length { display-menu-open-timer.running = false; root.display-menu-animation-enabled = false; root.display-menu-progress = 0.0; root.selected-display = index; root.switch-display(index); root.display-menu-open = false; } } scroll-event(event) => { accept } } } if root.shortcut-menu-open: StreamControlSurface { x: control.x + shortcut-button.x; y: root.shortcut-menu-y; width: 180px; height: root.shortcut-menu-height; z: 11; TouchArea { mouse-cursor: default; scroll-event(event) => { accept } } VerticalLayout { padding: 8px; spacing: 1px; for shortcut in root.shortcut-items: Rectangle { height: 29px; background: shortcut-touch.has-hover ? #e8eef9 : transparent; Text { x: 8px; text: shortcut; color: #30343b; font-size: ImGuiFontStyle.body; vertical-alignment: center; } shortcut-touch := TouchArea { mouse-cursor: MouseCursor.pointer; scroll-event(event) => { accept } clicked => { root.send-shortcut(shortcut); root.shortcut-menu-open = false; } } } } } // Measure unwrapped text so short hints stay compact. Render outside // the clipped control bar, capping longer hints before wrapping. control-tooltip-width-probe := Text { visible: false; text: root.hovered-control.text; font-size: ImGuiFontStyle.body; } if root.hovered-control.text != "" && root.control-tooltip-visible: Rectangle { width: min(240px, video.width - 8px, control-tooltip-width-probe.preferred-width + 16px); x: min(max(4px, control.x + root.hovered-control.anchor-x - self.width / 2), video.width - self.width - 4px); height: control-tip.preferred-height + 12px; // Anchor to the button row so expanded stats stay beneath the tooltip. y: control.y + root.control-toolbar-height + self.height + 8px <= video.height ? control.y + root.control-toolbar-height + 4px : max(4px, control.y - self.height - 4px); background: #30343bf5; border-radius: UiRadius.control; z: 12; control-tip := Text { x: 8px; y: 6px; width: parent.width - 16px; text: root.hovered-control.text; color: white; font-size: ImGuiFontStyle.body; wrap: word-wrap; } } if root.remote-update-visible: Rectangle { width: parent.width; height: parent.height; background: #0c162c52; z: 13; TouchArea { mouse-cursor: default; scroll-event(event) => { accept } } } remote-update-banner := DialogSurface { visible: root.remote-update-visible; x: (parent.width - self.width) / 2; y: min(max(64px, parent.height * 0.18), max(16px, parent.height - self.height - 16px)); width: min(400px, parent.width - 48px); height: remote-update-message.y + remote-update-message.height + 84px; border-color: #e5e9f0; border-radius: UiRadius.surface; drop-shadow-color: #07142b30; drop-shadow-blur: 32px; drop-shadow-offset-y: 12px; z: 14; TouchArea { mouse-cursor: default; scroll-event(event) => { accept } } remote-update-title := Text { x: 24px; y: 24px; width: parent.width - 80px; height: max(28px, self.preferred-height); text: StreamStrings.remote-update-title; color: #1b2433; font-size: 20px; font-weight: 600; wrap: word-wrap; } remote-update-message := Text { x: remote-update-title.x; y: remote-update-title.y + remote-update-title.height + 8px; width: parent.width - 48px; height: max(22px, self.preferred-height); text: StreamStrings.remote-update; color: #667085; font-size: 15px; wrap: word-wrap; } IconButton { x: parent.width - 44px; y: 12px; width: 32px; height: 32px; icon: FontAwesomeIcons.xmark; icon-size: 14px; icon-color: #8a93a3; hover-background: #f2f4f7; clicked => { root.dismiss-remote-update(); } } PrimaryButton { x: parent.width - 128px; y: parent.height - 60px; width: 104px; height: 36px; border-radius: UiRadius.control; text: StreamStrings.remote-update-dismiss; text-size: 14px; clicked => { root.dismiss-remote-update(); } } } // Transfer progress overlay at bottom-left. if root.file-transfer-visible: Rectangle { x: root.file-panel-x; y: root.file-panel-y; width: root.file-panel-width; height: root.file-panel-height; background: #fffffff0; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: UiRadius.surface; z: 7; // Header/padding and drags leaving the panel belong to local UI. TouchArea { mouse-cursor: default; scroll-event(event) => { accept } } Text { x: 12px; y: 8px; text: StreamStrings.file-transfer; color: #2d3138; font-weight: 600; } IconButton { x: parent.width - 32px; y: 1px; icon: FontAwesomeIcons.xmark; icon-size: 13px; clicked => { root.close-file-transfer(); } } ScrollView { x: 10px; y: 32px; width: parent.width - 20px; height: parent.height - 42px; VerticalLayout { // Slint's ScrollView draws its vertical scrollbar over // the viewport. Reserve a gutter so transfer content // never extends underneath it. padding-right: 16px; spacing: 8px; for entry in root.file-transfers: VerticalLayout { spacing: 3px; HorizontalLayout { height: 18px; Text { text: entry.name; color: #30343b; font-size: ImGuiFontStyle.body; overflow: elide; } Text { width: 80px; text: entry.status; color: #5c6470; font-size: ImGuiFontStyle.body; horizontal-alignment: right; } } Rectangle { height: 7px; background: #dfe3e8; border-radius: 3px; Rectangle { x: 0px; y: 0px; width: parent.width * max(0, min(1, entry.progress)); height: parent.height; background: #2463c7; border-radius: 3px; } } HorizontalLayout { height: 14px; Text { text: round(entry.progress * 100) + "% " + entry.speed; color: #6a717c; font-size: ImGuiFontStyle.small; } Text { text: entry.size; color: #6a717c; font-size: ImGuiFontStyle.small; horizontal-alignment: right; } } } } } } } } if root.custom-titlebar && !root.fullscreen-enabled: Rectangle { width: parent.width; height: parent.height; background: transparent; border-width: 1px; border-color: #aaa8a6; border-radius: root.window-maximized ? 0px : root.window-corner-radius; z: 200; } } }