diff --git a/apps/desktop/src/gui/ui/stream_window.slint b/apps/desktop/src/gui/ui/stream_window.slint index eb380727..1e419c68 100644 --- a/apps/desktop/src/gui/ui/stream_window.slint +++ b/apps/desktop/src/gui/ui/stream_window.slint @@ -47,6 +47,11 @@ export global StreamStrings { in-out property stats-connection-mode: "Mode"; } +struct ControlBarTooltip { + text: string, + anchor-x: length, +} + // The legacy ImGui control bar uses 24px buttons with Font Awesome rendered // at half of the 32px atlas size. Keep this local to the stream window: the // shared IconButton is intentionally larger and is used by the other windows. @@ -61,6 +66,10 @@ component ControlBarButton inherits Rectangle { 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.tooltip : "", + anchor-x: root.x + root.width / 2, + }; callback clicked; width: 24px; @@ -190,13 +199,25 @@ export component StreamWindow inherits Window { private property display-menu-animation-enabled: false; private property display-menu-progress: 0.0; private property shortcut-menu-open: false; - private property privacy-tooltip-visible: false; - private property privacy-hovered: root.control-expanded - && privacy-button.has-hover && !root.control-dragging; - private property privacy-tooltip-width: min(320px, video.width); - private property privacy-tooltip-x: min(max(0px, - control.x + (root.control-docked-left ? 105px : 138px)), - video.width - root.privacy-tooltip-width); + private property control-tooltip-visible: false; + private property hovered-control: { + if root.window-active && !root.control-dragging + && !root.display-menu-open && !root.shortcut-menu-open { + if collapse-button.has-hover { return collapse-button.tooltip-info; } + if root.control-expanded { + 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 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 }; + } // A popup can remove itself from the scene in its clicked callback before // the video input layer observes the same pointer release. Remember that // the press belonged to local UI so that release cannot reach the peer. @@ -236,11 +257,18 @@ export component StreamWindow inherits Window { } } - changed privacy-hovered => { root.privacy-tooltip-visible = false; } - privacy-tooltip-timer := Timer { + 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: root.privacy-hovered && !root.privacy-tooltip-visible; - triggered => { root.privacy-tooltip-visible = true; } + running: false; + triggered => { + self.running = false; + root.control-tooltip-visible = true; + } } is-local-control-area(pointer-x, pointer-y) => { @@ -685,7 +713,8 @@ export component StreamWindow inherits Window { background: ImGuiLineStyle.control-separator; } - if root.control-expanded: display-button := ControlBarButton { + display-button := ControlBarButton { + visible: root.control-expanded; x: root.control-docked-left ? 9px : 42px; y: 7px; icon: FontAwesomeIcons.display; badge: root.selected-display + 1; @@ -701,7 +730,8 @@ export component StreamWindow inherits Window { } } } - if root.control-expanded: shortcut-button := ControlBarButton { + shortcut-button := ControlBarButton { + visible: root.control-expanded; x: root.control-docked-left ? 41px : 74px; y: 7px; icon: FontAwesomeIcons.keyboard; tooltip: StreamStrings.send-shortcut; clicked => { @@ -712,36 +742,42 @@ export component StreamWindow inherits Window { root.display-menu-open = false; } } - if root.control-expanded: mouse-button := ControlBarButton { + mouse-button := ControlBarButton { + visible: root.control-expanded; x: root.control-docked-left ? 73px : 106px; y: 7px; icon: FontAwesomeIcons.computer-mouse; slashed: !root.mouse-control-enabled; tooltip: root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse; clicked => { root.toggle-mouse-control(); } } - if root.control-expanded: audio-button := ControlBarButton { + audio-button := ControlBarButton { + visible: root.control-expanded; x: root.control-docked-left ? 137px : 170px; y: 7px; icon: FontAwesomeIcons.volume-high; slashed: !root.audio-enabled; tooltip: root.audio-enabled ? StreamStrings.mute : StreamStrings.audio; clicked => { root.toggle-audio(); } } - if root.control-expanded: file-button := ControlBarButton { + file-button := ControlBarButton { + visible: root.control-expanded; x: root.control-docked-left ? 169px : 202px; y: 7px; icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file; clicked => { root.select-file(); } } - if root.control-expanded: stats-button := ControlBarButton { + stats-button := ControlBarButton { + visible: root.control-expanded; x: root.control-docked-left ? 201px : 234px; y: 7px; icon: FontAwesomeIcons.signal; selected: root.stats-visible; tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats; clicked => { root.toggle-stats(); } } - if root.control-expanded: fullscreen-button := ControlBarButton { + fullscreen-button := ControlBarButton { + visible: root.control-expanded; x: root.control-docked-left ? 233px : 266px; y: 7px; icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand; tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen; clicked => { root.toggle-fullscreen(); } } - if root.control-expanded: close-button := ControlBarButton { + close-button := ControlBarButton { + visible: root.control-expanded; x: root.control-docked-left ? 265px : 298px; y: 7px; icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect; clicked => { root.disconnect(); } @@ -769,7 +805,9 @@ export component StreamWindow inherits Window { slashed: root.privacy-off && !root.privacy-unsupported; muted: root.privacy-unsupported; action-enabled: root.privacy-can-toggle; - tooltip: root.privacy-status-text; + tooltip: root.privacy-status-text + + (root.privacy-can-toggle ? "\n" + (root.privacy-active + ? StreamStrings.privacy-disable : StreamStrings.privacy-enable) : ""); clicked => { root.toggle-privacy(); } } @@ -1001,18 +1039,25 @@ export component StreamWindow inherits Window { } } - if root.privacy-hovered && root.privacy-tooltip-visible: Rectangle { - x: root.privacy-tooltip-x; - width: root.privacy-tooltip-width; - height: privacy-tip.preferred-height + 16px; - y: control.y + control.height + self.height <= video.height - ? control.y + control.height : max(0px, control.y - self.height); + // 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; + y: control.y + control.height + self.height + 8px <= video.height + ? control.y + control.height + 4px : max(4px, control.y - self.height - 4px); background: #30343bf5; border-radius: 5px; z: 12; - privacy-tip := Text { - x: 8px; y: 8px; width: parent.width - 16px; - text: root.privacy-status-text - + (root.privacy-can-toggle ? "\n" + (root.privacy-active - ? StreamStrings.privacy-disable : StreamStrings.privacy-enable) : ""); + 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; } }