mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-24 05:33:08 +08:00
1855 lines
91 KiB
Plaintext
1855 lines
91 KiB
Plaintext
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 <string> remote-update-title: "Remote update recommended";
|
|
in-out property <string> remote-update: "Update the remote device to the latest version for improved features and performance.";
|
|
in-out property <string> remote-update-dismiss: "Got it";
|
|
in-out property <string> video-settings: "Video settings";
|
|
in-out property <string> video-settings-unavailable: "The remote device does not support live video settings";
|
|
in-out property <string> video-quality: "Video quality";
|
|
in-out property <string> frame-rate: "Capture frame rate";
|
|
in-out property <string> adaptation-policy: "Video preference";
|
|
in-out property <string> quality-low: "Low";
|
|
in-out property <string> quality-medium: "Medium";
|
|
in-out property <string> quality-high: "High";
|
|
in-out property <string> priority-frame-rate: "Frame rate";
|
|
in-out property <string> priority-quality: "Quality";
|
|
in-out property <string> priority-balanced: "Balanced";
|
|
|
|
in-out property <string> select-display: "Select Display";
|
|
in-out property <string> send-shortcut: "Send Shortcut";
|
|
in-out property <string> privacy-enable: "Enable privacy screen";
|
|
in-out property <string> privacy-disable: "Turn privacy off / recover";
|
|
in-out property <string> control-mouse: "Control Mouse";
|
|
in-out property <string> release-mouse: "Release Mouse";
|
|
in-out property <string> audio: "Audio Capture";
|
|
in-out property <string> mute: "Mute";
|
|
in-out property <string> select-file: "Select File to Send";
|
|
in-out property <string> show-stats: "Show Net Traffic Stats";
|
|
in-out property <string> hide-stats: "Hide Net Traffic Stats";
|
|
in-out property <string> fullscreen: "Fullscreen";
|
|
in-out property <string> exit-fullscreen: "Exit fullscreen";
|
|
in-out property <string> disconnect: "Disconnect";
|
|
in-out property <string> confirm-disconnect: "Disconnect this session?";
|
|
in-out property <string> disconnect-description: "The current remote session will end.";
|
|
in-out property <string> cancel: "Cancel";
|
|
in-out property <string> file-transfer: "File Transfer Progress";
|
|
in-out property <string> expand-control: "Expand Control Bar";
|
|
in-out property <string> collapse-control: "Collapse Control Bar";
|
|
in-out property <string> stats-in: "In";
|
|
in-out property <string> stats-out: "Out";
|
|
in-out property <string> stats-loss-rate: "Loss Rate";
|
|
in-out property <string> stats-resolution: "Res";
|
|
in-out property <string> stats-video-latency: "Video delay";
|
|
in-out property <string> stats-connection-latency: "Connection RTT";
|
|
in-out property <string> stats-connection-mode: "Mode";
|
|
in-out property <string> stats-encoder: "Remote encoder";
|
|
in-out property <string> stats-decoder: "Local decoder";
|
|
in-out property <string> stats-encryption: "Encryption";
|
|
in-out property <string> stats-encryption-enabled: "Enabled";
|
|
in-out property <string> 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 <string> icon;
|
|
in property <bool> show-logo: false;
|
|
in property <bool> logo-pressed: false;
|
|
in property <bool> logo-dragging: false;
|
|
in property <float> reveal-progress: 1;
|
|
in property <string> badge: "";
|
|
in property <string> tooltip: "";
|
|
in property <bool> selected: false;
|
|
in property <bool> danger: false;
|
|
in property <bool> slashed: false;
|
|
in property <bool> action-enabled: true;
|
|
// Capability and temporary command availability have different visuals.
|
|
in property <bool> muted: false;
|
|
in property <length> icon-size: 16px;
|
|
out property <bool> has-hover: touch.has-hover;
|
|
out property <ControlBarTooltip> 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 <bool> button-hovered:
|
|
!root.show-logo && root.action-enabled && !root.muted && touch.has-hover;
|
|
private property <bool> 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 <color> 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 <color> 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 <float> 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 <string> label;
|
|
in property <[string]> options;
|
|
in property <int> selected;
|
|
in property <bool> 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 <bool> custom-titlebar: false;
|
|
in property <bool> 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 <length> video-area-top: content-layer.y + video.y;
|
|
out property <length> video-area-width: video.width;
|
|
out property <length> video-area-height: video.height;
|
|
in property <bool> window-active: true;
|
|
in property <bool> window-maximized: false;
|
|
in property <[StreamTab]> tabs;
|
|
in-out property <int> selected-tab: 0;
|
|
in property <image> frame;
|
|
in property <bool> has-frame: false;
|
|
in property <string> status-text: "";
|
|
in property <string> receiving-text: "";
|
|
in property <bool> remote-update-visible: false;
|
|
callback dismiss-remote-update;
|
|
in property <[string]> displays;
|
|
in-out property <int> selected-display: 0;
|
|
in property <bool> mouse-control-enabled: true;
|
|
in property <bool> privacy-can-toggle: false;
|
|
in property <bool> privacy-active: false;
|
|
in property <bool> privacy-off: false;
|
|
in property <bool> privacy-unsupported: false;
|
|
// Privacy feedback stays on its toolbar button and hover tooltip.
|
|
in property <string> privacy-status-text: "Privacy screen";
|
|
// Numeric values mirror crossdesk::RemoteCursorShape and Slint's MouseCursor.
|
|
in property <bool> remote-cursor-active: false;
|
|
in property <int> remote-cursor-shape: 0;
|
|
in property <bool> audio-enabled: true;
|
|
in property <bool> video-settings-enabled: false;
|
|
in property <string> video-settings-feedback: "";
|
|
in-out property <int> video-quality-index: 2;
|
|
in-out property <int> frame-rate-index: 1;
|
|
in-out property <int> adaptation-policy-index: 1;
|
|
callback change-video-settings(int, int, int);
|
|
in property <bool> fullscreen-enabled: false;
|
|
in property <bool> stats-visible: false;
|
|
in property <[NetworkStatsRow]> stats-rows;
|
|
in property <string> stats-fps: "0";
|
|
in property <string> stats-resolution: "";
|
|
in property <string> stats-video-latency: "—";
|
|
in property <string> stats-connection-latency: "—";
|
|
in property <string> stats-connection-mode: "";
|
|
in property <string> stats-encoder: "—";
|
|
in property <string> stats-decoder: "—";
|
|
in property <bool> stats-encryption-enabled: false;
|
|
in property <bool> 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 <bool> 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 <length> 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 <bool> control-expanded: false;
|
|
private property <bool> control-visual-expanded: false;
|
|
private property <length> control-toolbar-height: 38px;
|
|
private property <length> control-button-step: 32px;
|
|
private property <length> control-disconnect-gap: 8px;
|
|
private property <length> control-buttons-x: root.control-docked-left ? 43px : 7px;
|
|
private property <length> control-panel-gap: 8px;
|
|
private property <float> control-reveal-progress: 0;
|
|
private property <float> control-surface-progress: min(1, root.control-reveal-progress / 0.65);
|
|
private property <float> control-content-progress: max(0, (root.control-reveal-progress - 0.65) / 0.35);
|
|
private property <bool> control-content-visible:
|
|
root.control-visual-expanded && root.control-content-progress > 0;
|
|
private property <length> control-surface-width: root.control-toolbar-height
|
|
+ (control.width - root.control-toolbar-height) * root.control-surface-progress;
|
|
private property <length> control-surface-x: control.x
|
|
+ (root.control-docked-left ? 0px : control.width - root.control-surface-width);
|
|
private property <duration> control-collapse-duration: 180ms;
|
|
private property <bool> control-animation-enabled: false;
|
|
private property <length> stats-row-height: 18px;
|
|
private property <length> stats-header-height: 18px;
|
|
private property <length> stats-content-height:
|
|
root.stats-header-height + root.stats-rows.length * root.stats-row-height + 114px;
|
|
private property <length> stats-panel-height:
|
|
min(root.stats-content-height + 16px, max(0px, video.height - root.control-toolbar-height - root.control-panel-gap));
|
|
private property <length> stats-panel-y: root.control-panel-y(root.stats-panel-height);
|
|
private property <length> file-panel-x: 18px;
|
|
private property <length> file-panel-y: video.height - min(210px, video.height * 0.35);
|
|
private property <length> file-panel-width: min(430px, video.width * 0.6);
|
|
private property <length> file-panel-height: min(190px, video.height * 0.32);
|
|
private property <bool> display-menu-open: false;
|
|
private property <bool> display-menu-animation-enabled: false;
|
|
private property <float> display-menu-progress: 0.0;
|
|
private property <length> display-menu-height: min(180px, root.displays.length * 30px + 16px);
|
|
private property <length> display-menu-y: root.control-panel-y(root.display-menu-height);
|
|
private property <bool> shortcut-menu-open: false;
|
|
private property <length> shortcut-menu-y: root.control-panel-y(root.shortcut-menu-height);
|
|
in-out property <bool> video-menu-open: false;
|
|
private property <length> video-menu-width: min(320px, video.width - 8px);
|
|
private property <length> video-menu-height: root.video-settings-enabled && root.video-settings-feedback == "" ? 182px : 220px;
|
|
private property <length> video-menu-x: min(max(4px, control.x + video-button.x), video.width - root.video-menu-width - 4px);
|
|
private property <length> video-menu-y: root.control-panel-y(root.video-menu-height);
|
|
private property <bool> disconnect-menu-open: false;
|
|
private property <length> disconnect-menu-width: min(220px, video.width - 8px);
|
|
private property <length> disconnect-menu-height:
|
|
disconnect-actions.y + disconnect-actions.height + 12px;
|
|
private property <length> 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 <length> disconnect-menu-y: root.control-panel-y(root.disconnect-menu-height);
|
|
private property <bool> 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 <bool> control-tooltip-visible: false;
|
|
private property <ControlBarTooltip> 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 <bool> remote-left-down: false;
|
|
private property <bool> remote-right-down: false;
|
|
private property <bool> remote-middle-down: false;
|
|
private property <bool> remote-pointer-active:
|
|
root.remote-left-down || root.remote-right-down || root.remote-middle-down;
|
|
private property <bool> control-drag-pending: false;
|
|
private property <length> control-drag-start-x: 0px;
|
|
private property <length> control-drag-start-y: 0px;
|
|
private property <bool> control-docked-left: true;
|
|
private property <bool> control-dragging: false;
|
|
private property <length> 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 <length> control-pos-y: 0px;
|
|
private property <length> control-press-x: 0px;
|
|
private property <length> control-press-y: 0px;
|
|
private property <int> dragging-tab: -1;
|
|
private property <length> titlebar-height:
|
|
root.custom-titlebar && !root.fullscreen-enabled ? 32px : 0px;
|
|
private property <length> 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 <int> 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 <length> row-height: root.stats-row-height;
|
|
private property <length> label-column-width: 50px;
|
|
private property <length> loss-column-width:
|
|
max(56px, stats-loss-label.preferred-width + 12px);
|
|
private property <length> value-column-width:
|
|
(self.width - self.label-column-width - self.loss-column-width) / 2;
|
|
private property <length> 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 <length> 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 <length> 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 <length> badge-padding: index >= 4 ? 6px : 0px;
|
|
private property <length> 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 <length> expanded-height: root.display-menu-height;
|
|
private property <int> 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;
|
|
}
|
|
}
|
|
}
|