[feat] add compact tooltips for all control bar buttons

This commit is contained in:
dijunkun
2026-09-12 22:39:44 +08:00
parent 56ebfdf8e7
commit 23bf2ff8b8
+76 -31
View File
@@ -47,6 +47,11 @@ export global StreamStrings {
in-out property <string> stats-connection-mode: "Mode"; in-out property <string> stats-connection-mode: "Mode";
} }
struct ControlBarTooltip {
text: string,
anchor-x: length,
}
// The legacy ImGui control bar uses 24px buttons with Font Awesome rendered // 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 // 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. // shared IconButton is intentionally larger and is used by the other windows.
@@ -61,6 +66,10 @@ component ControlBarButton inherits Rectangle {
in property <bool> muted: false; in property <bool> muted: false;
in property <length> icon-size: 16px; in property <length> icon-size: 16px;
out property <bool> has-hover: touch.has-hover; out property <bool> has-hover: touch.has-hover;
out property <ControlBarTooltip> tooltip-info: {
text: root.visible && touch.has-hover && !touch.pressed ? root.tooltip : "",
anchor-x: root.x + root.width / 2,
};
callback clicked; callback clicked;
width: 24px; width: 24px;
@@ -190,13 +199,25 @@ export component StreamWindow inherits Window {
private property <bool> display-menu-animation-enabled: false; private property <bool> display-menu-animation-enabled: false;
private property <float> display-menu-progress: 0.0; private property <float> display-menu-progress: 0.0;
private property <bool> shortcut-menu-open: false; private property <bool> shortcut-menu-open: false;
private property <bool> privacy-tooltip-visible: false; private property <bool> control-tooltip-visible: false;
private property <bool> privacy-hovered: root.control-expanded private property <ControlBarTooltip> hovered-control: {
&& privacy-button.has-hover && !root.control-dragging; if root.window-active && !root.control-dragging
private property <length> privacy-tooltip-width: min(320px, video.width); && !root.display-menu-open && !root.shortcut-menu-open {
private property <length> privacy-tooltip-x: min(max(0px, if collapse-button.has-hover { return collapse-button.tooltip-info; }
control.x + (root.control-docked-left ? 105px : 138px)), if root.control-expanded {
video.width - root.privacy-tooltip-width); 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 // 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 video input layer observes the same pointer release. Remember that
// the press belonged to local UI so that release cannot reach the peer. // 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; } changed hovered-control => {
privacy-tooltip-timer := Timer { 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; interval: 450ms;
running: root.privacy-hovered && !root.privacy-tooltip-visible; running: false;
triggered => { root.privacy-tooltip-visible = true; } triggered => {
self.running = false;
root.control-tooltip-visible = true;
}
} }
is-local-control-area(pointer-x, pointer-y) => { is-local-control-area(pointer-x, pointer-y) => {
@@ -685,7 +713,8 @@ export component StreamWindow inherits Window {
background: ImGuiLineStyle.control-separator; 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; x: root.control-docked-left ? 9px : 42px; y: 7px;
icon: FontAwesomeIcons.display; icon: FontAwesomeIcons.display;
badge: root.selected-display + 1; 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; x: root.control-docked-left ? 41px : 74px; y: 7px;
icon: FontAwesomeIcons.keyboard; tooltip: StreamStrings.send-shortcut; icon: FontAwesomeIcons.keyboard; tooltip: StreamStrings.send-shortcut;
clicked => { clicked => {
@@ -712,36 +742,42 @@ export component StreamWindow inherits Window {
root.display-menu-open = false; 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; x: root.control-docked-left ? 73px : 106px; y: 7px;
icon: FontAwesomeIcons.computer-mouse; slashed: !root.mouse-control-enabled; icon: FontAwesomeIcons.computer-mouse; slashed: !root.mouse-control-enabled;
tooltip: root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse; tooltip: root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse;
clicked => { root.toggle-mouse-control(); } 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; x: root.control-docked-left ? 137px : 170px; y: 7px;
icon: FontAwesomeIcons.volume-high; slashed: !root.audio-enabled; icon: FontAwesomeIcons.volume-high; slashed: !root.audio-enabled;
tooltip: root.audio-enabled ? StreamStrings.mute : StreamStrings.audio; tooltip: root.audio-enabled ? StreamStrings.mute : StreamStrings.audio;
clicked => { root.toggle-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; x: root.control-docked-left ? 169px : 202px; y: 7px;
icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file; icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file;
clicked => { root.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; x: root.control-docked-left ? 201px : 234px; y: 7px;
icon: FontAwesomeIcons.signal; selected: root.stats-visible; icon: FontAwesomeIcons.signal; selected: root.stats-visible;
tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats; tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats;
clicked => { root.toggle-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; x: root.control-docked-left ? 233px : 266px; y: 7px;
icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand; icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen; tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen;
clicked => { root.toggle-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; x: root.control-docked-left ? 265px : 298px; y: 7px;
icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect; icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect;
clicked => { root.disconnect(); } clicked => { root.disconnect(); }
@@ -769,7 +805,9 @@ export component StreamWindow inherits Window {
slashed: root.privacy-off && !root.privacy-unsupported; slashed: root.privacy-off && !root.privacy-unsupported;
muted: root.privacy-unsupported; muted: root.privacy-unsupported;
action-enabled: root.privacy-can-toggle; 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(); } clicked => { root.toggle-privacy(); }
} }
@@ -1001,18 +1039,25 @@ export component StreamWindow inherits Window {
} }
} }
if root.privacy-hovered && root.privacy-tooltip-visible: Rectangle { // Measure unwrapped text so short hints stay compact. Render outside
x: root.privacy-tooltip-x; // the clipped control bar, capping longer hints before wrapping.
width: root.privacy-tooltip-width; control-tooltip-width-probe := Text {
height: privacy-tip.preferred-height + 16px; visible: false;
y: control.y + control.height + self.height <= video.height text: root.hovered-control.text;
? control.y + control.height : max(0px, control.y - self.height); 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; background: #30343bf5; border-radius: 5px; z: 12;
privacy-tip := Text { control-tip := Text {
x: 8px; y: 8px; width: parent.width - 16px; x: 8px; y: 6px; width: parent.width - 16px;
text: root.privacy-status-text text: root.hovered-control.text;
+ (root.privacy-can-toggle ? "\n" + (root.privacy-active
? StreamStrings.privacy-disable : StreamStrings.privacy-enable) : "");
color: white; font-size: ImGuiFontStyle.body; wrap: word-wrap; color: white; font-size: ImGuiFontStyle.body; wrap: word-wrap;
} }
} }