import { ScrollView } from "std-widgets.slint"; import { FontAwesomeIcons, ImGuiLineStyle, FloatingBarStyle } from "common.slint"; export struct ControllerEntry { remote-id: string, display-name: string, } export global ServerStrings { in property controlled: "被控中"; in property heading: "此设备被控中"; in property connected: "已连接"; in property send-file: "发送文件"; in property disconnect: "断开"; in property disconnect-one: "断开连接"; in property disconnect-all: "全部断开"; in property confirm-disconnect: "断开所有控制端的连接?"; in property cancel: "取消"; } component SessionButton inherits Rectangle { in property text; in property danger: false; in property enabled: true; private property keyboard-focus: false; private property pointer-feedback: true; callback clicked; width: label.preferred-width + 20px; height: 28px; border-radius: 5px; border-width: 1px; border-color: root.danger ? #d92d20 : #c9ced7; background: root.danger ? (root.pointer-feedback && touch.pressed ? #b42318 : root.pointer-feedback && touch.has-hover ? #f04438 : #d92d20) : (root.pointer-feedback && touch.pressed ? #dfe3e9 : root.pointer-feedback && touch.has-hover ? #f3f4f6 : white); opacity: root.enabled ? 1 : 0.5; accessible-role: button; accessible-label: root.text; accessible-action-default => { if root.enabled { root.pointer-feedback = false; root.clicked(); } } label := Text { text: root.text; color: root.danger ? white : #24272d; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; } forward-focus: keyboard; keyboard := FocusScope { enabled: root.enabled; focus-gained(reason) => { if reason == FocusReason.tab-navigation { root.keyboard-focus = true; } else if reason == FocusReason.pointer-click { root.keyboard-focus = false; } // Preserve the input method when returning from the file picker. } key-pressed(event) => { root.keyboard-focus = true; if root.enabled && (event.text == Key.Return || event.text == " ") { root.pointer-feedback = false; root.clicked(); return accept; } return reject; } } touch := TouchArea { enabled: root.enabled; pointer-event(event) => { if event.kind == PointerEventKind.down || event.kind == PointerEventKind.move { root.pointer-feedback = true; } } // A native dialog may not deliver a pointer exit to the owner window. // Restore hover feedback only after a fresh pointer move or press. clicked => { root.pointer-feedback = false; root.keyboard-focus = false; keyboard.focus(); root.clicked(); } } if root.enabled && keyboard.has-focus && root.keyboard-focus: Rectangle { x: 2px; y: 2px; width: parent.width - 4px; height: parent.height - 4px; border-radius: 3px; border-width: 1px; border-color: root.danger ? white : #2463c7; } } // A separate native window keeps the collapsed footprint as small as the bar. export component ServerStatusBar inherits Window { title: "CrossDesk"; no-frame: true; always-on-top: true; background: transparent; in property controller-count: 1; in property expanded: false; in property opens-above: false; in property dragged: false; out property bar-width: label.preferred-width + 48px; preferred-width: root.bar-width; preferred-height: 36px; min-width: root.bar-width; max-width: root.bar-width; min-height: 36px; max-height: 36px; callback toggle-panel; callback dismiss-panel; callback title-drag(int, length, length); callback move-anchor(float); forward-focus: keyboard; keyboard := FocusScope { key-pressed(event) => { if event.text == Key.Return || event.text == " " { root.toggle-panel(); return accept; } if event.text == Key.Escape { root.dismiss-panel(); return accept; } if event.text == Key.UpArrow { root.move-anchor(-0.05); return accept; } if event.text == Key.DownArrow { root.move-anchor(0.05); return accept; } return reject; } } Rectangle { x: 0px; y: 0px; width: parent.width; height: parent.height; background: touch.pressed ? #dfe3e9 : touch.has-hover ? #f3f4f6 : white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: FloatingBarStyle.corner-radius; border-top-right-radius: 0px; border-bottom-right-radius: 0px; } label := Text { x: 10px; width: root.width - 38px; height: root.height; text: ServerStrings.controlled + (root.controller-count > 1 ? " · " + root.controller-count : ""); color: #24272d; font-size: 14px; vertical-alignment: center; } Text { x: root.width - 26px; width: 18px; height: root.height; text: root.opens-above != root.expanded ? FontAwesomeIcons.angle-up : FontAwesomeIcons.angle-down; color: #24272d; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; } touch := TouchArea { property phase: self.pressed ? 1 : 0; changed phase => { root.title-drag(self.phase, self.mouse-x, self.mouse-y); } moved => { if self.pressed { root.title-drag(2, self.mouse-x, self.mouse-y); } } clicked => { keyboard.focus(); if !root.dragged { root.toggle-panel(); } } } accessible-role: button; accessible-label: label.text; accessible-action-default => { root.toggle-panel(); } } export component ServerWindow inherits Window { title: "CrossDesk"; icon: @image-url("../../../resources/linux/crossdesk_256x256.png"); preferred-width: 225px; min-width: 225px; max-width: 225px; preferred-height: root.content-height; no-frame: true; always-on-top: true; background: transparent; default-font-size: 12px; in property <[ControllerEntry]> controllers; in-out property selected-controller: 0; in property sending-file: false; in property file-transfer-visible: false; in property file-progress: 0; in property file-progress-text; in property current-file-name; in property file-size-text; in-out property confirming-disconnect: false; private property footer-height: send-file.width + disconnect-all.width + 6px > 205px ? 62px : 28px; private property hovered-name; private property tooltip-y; out property content-height: 10px + 24px + (root.controllers.length > 1 ? min(root.controllers.length, 6) * 34px : 28px) + 8px + root.footer-height + 10px + (root.confirming-disconnect ? 36px : 0px) + (root.file-transfer-visible ? 58px : 0px); callback controller-selected(int); callback select-file; callback disconnect-controller(string); callback disconnect-all; callback dismiss-panel; forward-focus: keyboard; keyboard := FocusScope { key-pressed(event) => { if event.text == Key.Escape { root.dismiss-panel(); return accept; } return reject; } Rectangle { width: root.width; height: root.height; background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 8px; Text { x: 10px; y: 10px; width: parent.width - 20px; height: 24px; text: ServerStrings.heading + (root.controllers.length > 1 ? " · " + root.controllers.length : ""); color: #24272d; font-size: 13px; font-weight: 500; vertical-alignment: center; overflow: elide; } list := ScrollView { x: 10px; y: 34px; width: parent.width - 20px; height: max(0px, root.height - 34px - root.footer-height - 18px - (root.confirming-disconnect ? 36px : 0px) - (root.file-transfer-visible ? 58px : 0px)); viewport-width: self.visible-width; viewport-height: root.controllers.length * (root.controllers.length > 1 ? 34px : 28px); for controller[index] in root.controllers: row := Rectangle { x: 0px; y: index * (root.controllers.length > 1 ? 34px : 28px); width: list.visible-width; height: root.controllers.length > 1 ? 32px : 28px; select := Rectangle { x: 0px; y: 0px; width: parent.width - (root.controllers.length > 1 ? disconnect.width + 4px : status.preferred-width + 8px); height: parent.height; border-radius: 4px; background: root.controllers.length > 1 && index == root.selected-controller ? #eef5fd : select-touch.has-hover ? #f3f4f6 : transparent; if root.controllers.length > 1: Rectangle { x: 4px; y: (parent.height - 13px) / 2; width: 13px; height: 13px; border-radius: 7px; border-width: 1px; border-color: index == root.selected-controller ? #2463c7 : #8c939e; if index == root.selected-controller: Rectangle { x: 3px; y: 3px; width: 7px; height: 7px; border-radius: 4px; background: #2463c7; } } name := Text { x: root.controllers.length > 1 ? 23px : 0px; width: parent.width - self.x - 4px; height: parent.height; text: controller.display-name; color: #24272d; font-size: 13px; vertical-alignment: center; overflow: elide; } forward-focus: selection-keyboard; selection-keyboard := FocusScope { key-pressed(event) => { if event.text == Key.Return || event.text == " " { root.selected-controller = index; root.controller-selected(index); return accept; } if event.text == Key.UpArrow || event.text == Key.DownArrow { root.selected-controller = max(0, min(root.controllers.length - 1, root.selected-controller + (event.text == Key.UpArrow ? -1 : 1))); root.controller-selected(root.selected-controller); return accept; } return reject; } } select-touch := TouchArea { clicked => { selection-keyboard.focus(); root.selected-controller = index; root.controller-selected(index); } changed has-hover => { root.hovered-name = self.has-hover && name.preferred-width > name.width ? controller.display-name : ""; root.tooltip-y = max(0px, min(root.height - 52px, list.y + row.y + list.viewport-y + row.height)); } } accessible-role: radio-button; accessible-label: controller.display-name; accessible-checked: index == root.selected-controller; accessible-action-default => { root.selected-controller = index; root.controller-selected(index); } } status := Text { visible: root.controllers.length == 1; x: parent.width - self.preferred-width; height: parent.height; text: ServerStrings.connected; color: #16a34a; font-size: 12px; vertical-alignment: center; } disconnect := SessionButton { visible: root.controllers.length > 1; x: parent.width - self.width; y: 2px; text: ServerStrings.disconnect; danger: true; clicked => { root.confirming-disconnect = false; root.disconnect-controller(controller.remote-id); } } } } footer := Rectangle { x: 10px; y: list.y + list.height + 8px; width: parent.width - 20px; height: root.footer-height; send-file := SessionButton { x: root.footer-height > 28px ? parent.width - self.width : disconnect-all.x - self.width - 6px; y: 0px; text: root.confirming-disconnect ? ServerStrings.cancel : ServerStrings.send-file; enabled: root.confirming-disconnect || !root.sending-file; clicked => { if root.confirming-disconnect { root.confirming-disconnect = false; } else { root.select-file(); } } } disconnect-all := SessionButton { x: parent.width - self.width; y: root.footer-height - 28px; text: root.controllers.length > 1 ? ServerStrings.disconnect-all : ServerStrings.disconnect-one; danger: true; clicked => { if root.controllers.length == 1 { root.disconnect-controller(root.controllers[0].remote-id); } else if root.confirming-disconnect { root.confirming-disconnect = false; root.disconnect-all(); } else { root.confirming-disconnect = true; } } } } if root.confirming-disconnect: Text { x: 10px; y: footer.y + footer.height + 6px; width: parent.width - 20px; height: 30px; text: ServerStrings.confirm-disconnect; color: #d92d20; font-size: 12px; wrap: word-wrap; } if root.file-transfer-visible: VerticalLayout { x: 10px; y: root.height - 62px; width: parent.width - 20px; height: 52px; spacing: 2px; Text { text: root.current-file-name; color: #596575; font-size: 11px; overflow: elide; } Text { text: root.file-size-text; color: #596575; font-size: 11px; overflow: elide; } Text { text: round(root.file-progress * 100) + "% " + root.file-progress-text; color: #2463c7; font-size: 11px; overflow: elide; } } } // Draw outside the scrolling list so its last row can reveal its name. if root.hovered-name != "": Rectangle { x: 10px; y: root.tooltip-y; width: 205px; height: tip.preferred-height + 12px; z: 20; background: #24272d; border-radius: 5px; tip := Text { x: 6px; y: 6px; width: parent.width - 12px; text: root.hovered-name; color: white; font-size: 12px; wrap: word-wrap; } } } }