Commit 4434355 for novnc.com

commit 44343556e0356e578ca40d454d90e4993ba2db12
Author: Tobias <tobfa@cendio.se>
Date:   Wed Feb 18 10:50:23 2026 +0100

    Enable attaching control bar to top and bottom

diff --git a/app/styles/base.css b/app/styles/base.css
index 5834935..9fd9fb3 100644
--- a/app/styles/base.css
+++ b/app/styles/base.css
@@ -117,7 +117,8 @@ html {
 .noVNC_center > * {
     pointer-events: auto;
 }
-.noVNC_vcenter {
+
+.noVNC_crosscenter {
     display: flex !important;
     flex-direction: column;
     justify-content: center;
@@ -129,9 +130,29 @@ html {
     padding: 0 !important;
     pointer-events: none;
 }
-.noVNC_vcenter > * {
+.noVNC_crosscenter > * {
     pointer-events: auto;
 }
+.noVNC_right .noVNC_crosscenter {
+    left: auto;
+    right: 0;
+}
+.noVNC_top.noVNC_crosscenter,
+.noVNC_top .noVNC_crosscenter {
+    flex-direction: row;
+    width: 100%;
+    height: auto;
+}
+.noVNC_bottom.noVNC_crosscenter,
+.noVNC_bottom .noVNC_crosscenter {
+    flex-direction: row;
+    width: 100%;
+    height: auto;
+}
+.noVNC_bottom .noVNC_crosscenter {
+    top: auto;
+    bottom: 0;
+}

 /* ----------------------------------------
  * Layering
@@ -231,10 +252,18 @@ html {
 :root.noVNC_connected #noVNC_control_bar_anchor.noVNC_idle {
     opacity: 0.8;
 }
+#noVNC_control_bar_anchor:is(.noVNC_top, .noVNC_bottom) {
+    /* Edge misrenders animations wihthout this */
+    transform: translateY(0);
+}
 #noVNC_control_bar_anchor.noVNC_right {
     left: auto;
     right: 0;
 }
+#noVNC_control_bar_anchor.noVNC_bottom {
+    top: auto;
+    bottom: 0;
+}

 #noVNC_control_bar {
     position: relative;
@@ -249,10 +278,34 @@ html {
     -webkit-user-select: none;
     -webkit-touch-callout: none; /* Disable iOS image long-press popup */
 }
+.noVNC_right #noVNC_control_bar {
+    left: 100%;
+    border-radius: 12px 0 0 12px;
+}
+.noVNC_top #noVNC_control_bar {
+    left: auto;
+    /* FIXME: We want to mirror the left and right modes here and use a
+              relative top offset (-100%), but it doesn't resolve
+              correctly against the anchor height reference */
+    top: -55px;
+    border-radius: 0 0 12px 12px;
+}
+.noVNC_bottom #noVNC_control_bar {
+    left: auto;
+    /* FIXME: We want to mirror the left and right modes here and use a
+              relative top offset (100%), but it doesn't resolve
+              correctly against the anchor height reference */
+    top: 55px;
+    border-radius: 12px 12px 0 0;
+}
 #noVNC_control_bar.noVNC_open {
     box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
     left: 0;
 }
+:is(.noVNC_top, .noVNC_bottom) #noVNC_control_bar.noVNC_open {
+    left: auto;
+    top: 0;
+}
 #noVNC_control_bar::before {
     /* This extra element is to get a proper shadow */
     content: "";
@@ -263,20 +316,23 @@ html {
     left: -30px;
     transition: box-shadow 0.5s ease-in-out;
 }
-#noVNC_control_bar.noVNC_open::before {
-    box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
-}
-.noVNC_right #noVNC_control_bar {
-    left: 100%;
-    border-radius: 12px 0 0 12px;
+.noVNC_right #noVNC_control_bar::before {
+    visibility: hidden;
 }
-.noVNC_right #noVNC_control_bar.noVNC_open {
-    left: 0;
+.noVNC_top #noVNC_control_bar::before {
+    height: 30px;
+    width: 100%;
+    top: -30px;
+    bottom: auto;
 }
-.noVNC_right #noVNC_control_bar::before {
+.noVNC_bottom #noVNC_control_bar::before {
     visibility: hidden;
 }

+#noVNC_control_bar.noVNC_open::before {
+    box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+}
+
 #noVNC_control_bar_handle {
     position: absolute;
     left: -15px;
@@ -288,41 +344,96 @@ html {
     cursor: pointer;
     border-radius: 6px;
     background-color: var(--novnc-darkblue);
-    background-image: url("../images/handle_bg.svg");
-    background-repeat: no-repeat;
-    background-position: right;
     box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5);
 }
+:root:not(.noVNC_connected) #noVNC_control_bar_handle {
+    display: none;
+}
+:is(.noVNC_top, .noVNC_bottom) #noVNC_control_bar_handle {
+    transform: translateX(35px);
+    top: -15px;
+    left: 0;
+    width: 50px;
+    height: calc(100% + 30px);
+}
+
+#noVNC_control_bar_handle::before {
+    content: "";
+    background: url("../images/handle_bg.svg");
+    background-repeat: no-repeat;
+    position: absolute;
+    top: 0;
+    right: 0;
+    width: 15px;
+    height: 50px;
+}
+.noVNC_right #noVNC_control_bar_handle::before {
+    left: 0;
+    right: auto;
+}
+.noVNC_top #noVNC_control_bar_handle::before {
+    left: 0;
+    right: auto;
+    transform-origin: bottom left;
+    transform: rotate(90deg) translateX(20px);
+}
+.noVNC_bottom #noVNC_control_bar_handle::before {
+    left: 0;
+    right: auto;
+    transform-origin: bottom left;
+    transform: rotate(90deg) translateX(-50px);
+}
+
 #noVNC_control_bar_handle:after {
     content: "";
     transition: transform 0.5s ease-in-out;
-    background: url("../images/handle.svg");
+    background: url("../images/handle.svg") no-repeat center;
+    background-size: 5px 6px;
     position: absolute;
-    top: 22px; /* (50px-6px)/2 */
-    right: 5px;
-    width: 5px;
-    height: 6px;
+    top: 20px; /* (50px-10px)/2 */
+    right: 3px;
+    transform: none;
+    width: 10px;
+    height: 10px;
+    transform-origin: center;
 }
-#noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
-    transform: translateX(1px) rotate(180deg);
+.noVNC_right #noVNC_control_bar_handle:after {
+    left: 3px;
+    right: auto;
+    transform: rotate(180deg);
 }
-:root:not(.noVNC_connected) #noVNC_control_bar_handle {
-    display: none;
+.noVNC_top #noVNC_control_bar_handle:after {
+    left: 20px;
+    right: auto;
+    top: auto;
+    bottom: 3px;
+    transform: rotate(90deg);
 }
-.noVNC_right #noVNC_control_bar_handle {
-    background-position: left;
+.noVNC_bottom #noVNC_control_bar_handle:after {
+    left: 20px;
+    right: auto;
+    top: 3px;
+    bottom: auto;
+    transform: rotate(-90deg);
 }
-.noVNC_right #noVNC_control_bar_handle:after {
-    left: 5px;
-    right: 0;
+
+#noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
     transform: translateX(1px) rotate(180deg);
 }
 .noVNC_right #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
-    transform: none;
+    transform: translateX(-1px);
+}
+.noVNC_top #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
+    transform: translateY(1px) rotate(-90deg);
 }
+.noVNC_bottom #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
+    transform: translateY(-1px) rotate(90deg);
+}
+
 /* Larger touch area for the handle, used when a touch screen is available */
 #noVNC_control_bar_handle div {
     position: absolute;
+    left: auto;
     right: -35px;
     top: 0;
     width: 50px;
@@ -338,6 +449,22 @@ html {
     left: -35px;
     right: auto;
 }
+.noVNC_top #noVNC_control_bar_handle div {
+    left: 0;
+    right: auto;
+    top: auto;
+    bottom: -35px;
+    width: 100%;
+    height: 50px;
+}
+.noVNC_bottom #noVNC_control_bar_handle div {
+    left: 0;
+    right: auto;
+    top: -35px;
+    bottom: auto;
+    width: 100%;
+    height: 50px;
+}

 #noVNC_control_bar > .noVNC_scroll {
     max-height: 100vh; /* Chrome is buggy with 100% */
@@ -350,23 +477,38 @@ html {
     align-items: center;
     gap: 10px 0;
 }
+:is(.noVNC_top, .noVNC_bottom) > #noVNC_control_bar > .noVNC_scroll {
+    max-width: 100vw; /* Chrome is buggy with 100% */
+    overflow-x: auto;
+    overflow-y: hidden;
+    flex-direction: row;
+    gap: 0 10px;
+}

 /* Control bar hint */
-#noVNC_hint_anchor {
+.noVNC_hint_anchor {
     position: fixed;
+    left: -50px;
+}
+.noVNC_hint_anchor.noVNC_right {
+    left: auto;
     right: -50px;
+}
+.noVNC_hint_anchor.noVNC_top {
     left: auto;
+    top: -50px;
 }
-#noVNC_control_bar_anchor.noVNC_right + #noVNC_hint_anchor {
-    left: -50px;
-    right: auto;
+.noVNC_hint_anchor.noVNC_bottom {
+    left: auto;
+    top: auto;
+    bottom: -50px;
 }
-#noVNC_control_bar_hint {
-    position: relative;
-    transform: scale(0);
+.noVNC_control_bar_hint {
     width: 100px;
     height: 50%;
     max-height: 600px;
+    position: relative;
+    transform: scale(0);

     visibility: hidden;
     opacity: 0;
@@ -376,13 +518,19 @@ html {
     border-radius: 12px;
     transition-delay: 0s;
 }
-#noVNC_control_bar_hint.noVNC_active {
+:is(.noVNC_top, .noVNC_bottom) .noVNC_control_bar_hint {
+    width: 50%;
+    height: 100px;
+    max-width: 600px;
+    max-height: none;
+}
+.noVNC_control_bar_hint.noVNC_active {
     visibility: visible;
     opacity: 1;
     transition-delay: 0.2s;
     transform: scale(1);
 }
-#noVNC_control_bar_hint.noVNC_notransition {
+.noVNC_control_bar_hint.noVNC_notransition {
     transition: none !important;
 }

@@ -390,7 +538,6 @@ html {
 #noVNC_control_bar .noVNC_button {
     min-width: unset;
     padding: 4px 4px;
-    vertical-align: middle;
     border:1px solid rgba(255, 255, 255, 0.2);
     border-radius: 6px;
     background-color: transparent;
@@ -411,7 +558,7 @@ html {

     box-sizing: border-box; /* so max-width don't have to care about padding */
     max-width: calc(100vw - 75px - 25px); /* minus left and right margins */
-    max-height: 100vh; /* Chrome is buggy with 100% */
+    max-height: calc(100vh - 75px - 25px); /* minus top and bottom margins */
     overflow-x: hidden;
     overflow-y: auto;

@@ -431,16 +578,24 @@ html {
     opacity: 1;
     transform: translateX(75px);
 }
-.noVNC_right .noVNC_vcenter {
-    left: auto;
-    right: 0;
-}
 .noVNC_right .noVNC_panel {
     transform: translateX(-25px);
 }
 .noVNC_right .noVNC_panel.noVNC_open {
     transform: translateX(-75px);
 }
+.noVNC_top .noVNC_panel {
+    transform: translateY(25px);
+}
+.noVNC_top .noVNC_panel.noVNC_open {
+    transform: translateY(75px);
+}
+.noVNC_bottom .noVNC_panel {
+    transform: translateY(-25px);
+}
+.noVNC_bottom .noVNC_panel.noVNC_open {
+    transform: translateY(-75px);
+}

 .noVNC_panel > * {
     display: block;
@@ -546,9 +701,17 @@ html {
     /* Remove all but top border */
     border: none;
     border-top: 1px solid rgba(255, 255, 255, 0.2);
-    width: 100%;
+    width: 35px;
+    height: 1px;
     margin: 0;
 }
+:is(.noVNC_top, .noVNC_bottom) .noVNC_logo + hr {
+    /* Remove all but left border */
+    border-left: 1px solid rgba(255, 255, 255, 0.2);
+    border-top: none;
+    width: 1px;
+    height: 35px;
+}

 :root:not(.noVNC_connected) #noVNC_view_drag_button {
     display: none;
@@ -587,6 +750,10 @@ html {
 #noVNC_modifiers > * {
     margin: 0;
 }
+:is(.noVNC_top, .noVNC_bottom) #noVNC_modifiers {
+    flex-direction: row;
+    gap: 0 10px;
+}

 /* Shutdown/Reboot */
 :root:not(.noVNC_connected) #noVNC_power_button {
diff --git a/app/ui.js b/app/ui.js
index 24d32d5..2f9e602 100644
--- a/app/ui.js
+++ b/app/ui.js
@@ -37,6 +37,8 @@ const UI = {

     controlbarGrabbed: false,
     controlbarDrag: false,
+    controlbarMouseDownClientX: 0,
+    controlbarMouseDownOffsetX: 0,
     controlbarMouseDownClientY: 0,
     controlbarMouseDownOffsetY: 0,

@@ -110,8 +112,11 @@ const UI = {
         }

         // Restore control bar position
-        if (WebUtil.readSetting('controlbar_pos') === 'right') {
-            UI.toggleControlbarSide();
+        const pos = WebUtil.readSetting('controlbar_pos');
+        if (['left', 'right', 'top', 'bottom'].includes(pos)) {
+            UI.toggleControlbarSide(pos);
+        } else {
+            UI.toggleControlbarSide('left');
         }

         UI.initFullscreen();
@@ -575,7 +580,15 @@ const UI = {
         }
     },

-    toggleControlbarSide() {
+    getControlbarPos() {
+        const anchor = document.getElementById('noVNC_control_bar_anchor');
+        if (anchor.classList.contains('noVNC_right')) return 'right';
+        if (anchor.classList.contains('noVNC_top')) return 'top';
+        if (anchor.classList.contains('noVNC_bottom')) return 'bottom';
+        return 'left';
+    },
+
+    toggleControlbarSide(pos) {
         // Temporarily disable animation, if bar is displayed, to avoid weird
         // movement. The transitionend-event will not fire when display=none.
         const bar = document.getElementById('noVNC_control_bar');
@@ -586,13 +599,12 @@ const UI = {
         }

         const anchor = document.getElementById('noVNC_control_bar_anchor');
-        if (anchor.classList.contains("noVNC_right")) {
-            WebUtil.writeSetting('controlbar_pos', 'left');
-            anchor.classList.remove("noVNC_right");
-        } else {
-            WebUtil.writeSetting('controlbar_pos', 'right');
-            anchor.classList.add("noVNC_right");
+
+        anchor.classList.remove('noVNC_right', 'noVNC_top', 'noVNC_bottom');
+        if (['right', 'top', 'bottom'].includes(pos)) {
+            anchor.classList.add(`noVNC_${pos}`);
         }
+        WebUtil.writeSetting('controlbar_pos', pos);

         // Consider this a movement of the handle
         UI.controlbarDrag = true;
@@ -602,19 +614,21 @@ const UI = {
     },

     showControlbarHint(show, animate=true) {
-        const hint = document.getElementById('noVNC_control_bar_hint');
+        const getPos = element =>
+            ['right', 'top', 'bottom'].find(pos =>
+                element.classList.contains(`noVNC_${pos}`)
+            ) ?? 'left';

-        if (animate) {
-            hint.classList.remove("noVNC_notransition");
-        } else {
-            hint.classList.add("noVNC_notransition");
-        }
+        const anchor = document.getElementById('noVNC_control_bar_anchor');
+        const anchorPos = getPos(anchor);

-        if (show) {
-            hint.classList.add("noVNC_active");
-        } else {
-            hint.classList.remove("noVNC_active");
-        }
+        document.querySelectorAll('.noVNC_control_bar_hint').forEach((hint) => {
+            const hintPos = getPos(hint.parentElement);
+            const shouldShow = show && (hintPos !== anchorPos);
+
+            hint.classList.toggle('noVNC_active', shouldShow);
+            hint.classList.toggle('noVNC_notransition', !animate || !shouldShow);
+        });
     },

     dragControlbarHandle(e) {
@@ -622,28 +636,62 @@ const UI = {

         const ptr = getPointerEvent(e);

-        const anchor = document.getElementById('noVNC_control_bar_anchor');
-        if (ptr.clientX < (window.innerWidth * 0.1)) {
-            if (anchor.classList.contains("noVNC_right")) {
-                UI.toggleControlbarSide();
+        let controlBarPos = UI.getControlbarPos();
+
+        if (ptr.clientX < (window.innerWidth * 0.1) &&
+            ptr.clientY > (window.innerHeight * 0.25) &&
+            ptr.clientY < (window.innerHeight * 0.75)) {
+            if (controlBarPos !== 'left') {
+                UI.toggleControlbarSide('left');
+                controlBarPos = 'left';
+            }
+
+        } else if (ptr.clientX > (window.innerWidth * 0.9) &&
+                   ptr.clientY > (window.innerHeight * 0.25) &&
+                   ptr.clientY < (window.innerHeight * 0.75)) {
+            if (controlBarPos !== 'right') {
+                UI.toggleControlbarSide('right');
+                controlBarPos = 'right';
             }
-        } else if (ptr.clientX > (window.innerWidth * 0.9)) {
-            if (!anchor.classList.contains("noVNC_right")) {
-                UI.toggleControlbarSide();
+
+        // Slightly increased height thresholds since 10% of the
+        // height proved small in practice
+        } else if (ptr.clientX > (window.innerWidth * 0.25) &&
+                   ptr.clientX < (window.innerWidth * 0.75) &&
+                   ptr.clientY < (window.innerHeight * 0.2)) {
+            if (controlBarPos !== 'top') {
+                UI.toggleControlbarSide('top');
+                controlBarPos = 'top';
+            }
+
+        } else if (ptr.clientX > (window.innerWidth * 0.25) &&
+                   ptr.clientX < (window.innerWidth * 0.75) &&
+                   ptr.clientY > (window.innerHeight * 0.8)) {
+            if (controlBarPos !== 'bottom') {
+                UI.toggleControlbarSide("bottom");
+                controlBarPos = 'bottom';
             }
         }

+        const isVertical = controlBarPos === 'left' || controlBarPos === 'right';
+
         if (!UI.controlbarDrag) {
-            const dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY);
+            const dragDistance = isVertical
+                ? Math.abs(ptr.clientY - UI.controlbarMouseDownClientY)
+                : Math.abs(ptr.clientX - UI.controlbarMouseDownClientX);

             if (dragDistance < dragThreshold) return;

             UI.controlbarDrag = true;
         }

-        const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
-
-        UI.moveControlbarHandle(eventY);
+        if (isVertical) {
+            const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
+            UI.moveControlbarHandle(eventY, true);
+        } else {
+            const eventX = ptr.clientX - UI.controlbarMouseDownOffsetX;
+            UI.moveControlbarHandle(eventX, false);
+        }

         e.preventDefault();
         e.stopPropagation();
@@ -652,41 +700,56 @@ const UI = {
     },

     // Move the handle but don't allow any position outside the bounds
-    moveControlbarHandle(viewportRelativeY) {
+    moveControlbarHandle(viewportRelativeCoord, isVertical) {
         const handle = document.getElementById("noVNC_control_bar_handle");
-        const handleHeight = handle.getBoundingClientRect().height;
+
+        const handleSpan = isVertical
+            ? handle.getBoundingClientRect().height
+            : handle.getBoundingClientRect().width;
+
         const controlbarBounds = document.getElementById("noVNC_control_bar")
             .getBoundingClientRect();
+        const controlbarBoundsStart = isVertical
+            ? controlbarBounds.top
+            : controlbarBounds.left;
+        const controlbarBoundsSpan  = isVertical
+            ? controlbarBounds.height
+            : controlbarBounds.width;
+
         const margin = 10;

         // These heights need to be non-zero for the below logic to work
-        if (handleHeight === 0 || controlbarBounds.height === 0) {
+        if (handleSpan === 0 || controlbarBoundsSpan === 0) {
             return;
         }

-        let newY = viewportRelativeY;
+        let newCoord = viewportRelativeCoord;

         // Check if the coordinates are outside the control bar
-        if (newY < controlbarBounds.top + margin) {
-            // Force coordinates to be below the top of the control bar
-            newY = controlbarBounds.top + margin;
+        if (newCoord < controlbarBoundsStart + margin) {
+            // Force coordinates to be below the start of the control bar
+            newCoord = controlbarBoundsStart + margin;

-        } else if (newY > controlbarBounds.top +
-                   controlbarBounds.height - handleHeight - margin) {
-            // Force coordinates to be above the bottom of the control bar
-            newY = controlbarBounds.top +
-                controlbarBounds.height - handleHeight - margin;
+        } else if (newCoord > controlbarBoundsStart +
+                   controlbarBoundsSpan - handleSpan - margin) {
+            // Force coordinates to be before the end of the control bar
+            newCoord = controlbarBoundsStart +
+                controlbarBoundsSpan - handleSpan - margin;
         }

         // Corner case: control bar too small for stable position
-        if (controlbarBounds.height < (handleHeight + margin * 2)) {
-            newY = controlbarBounds.top +
-                (controlbarBounds.height - handleHeight) / 2;
+        if (controlbarBoundsSpan < (handleSpan + margin * 2)) {
+            newCoord = controlbarBoundsStart +
+                (controlbarBoundsSpan - handleSpan) / 2;
         }

         // The transform needs coordinates that are relative to the parent
-        const parentRelativeY = newY - controlbarBounds.top;
-        handle.style.transform = "translateY(" + parentRelativeY + "px)";
+        const parentRelativeCoord = newCoord - controlbarBoundsStart;
+        if (isVertical) {
+            handle.style.transform = "translateY(" + parentRelativeCoord + "px)";
+        } else {
+            handle.style.transform = "translateX(" + parentRelativeCoord + "px)";
+        }
     },

     updateControlbarHandle() {
@@ -694,7 +757,15 @@ const UI = {
         // the move function expects coordinates relative the the viewport.
         const handle = document.getElementById("noVNC_control_bar_handle");
         const handleBounds = handle.getBoundingClientRect();
-        UI.moveControlbarHandle(handleBounds.top);
+
+        const controlBarPos = UI.getControlbarPos();
+        const isVertical = controlBarPos === 'left' || controlBarPos === 'right';
+
+        if (isVertical) {
+            UI.moveControlbarHandle(handleBounds.top, true);
+        } else {
+            UI.moveControlbarHandle(handleBounds.left, false);
+        }
     },

     controlbarHandleMouseUp(e) {
@@ -732,6 +803,8 @@ const UI = {

         UI.controlbarMouseDownClientY = ptr.clientY;
         UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top;
+        UI.controlbarMouseDownClientX = ptr.clientX;
+        UI.controlbarMouseDownOffsetX = ptr.clientX - bounds.left;
         e.preventDefault();
         e.stopPropagation();
         UI.keepControlbar();
diff --git a/vnc.html b/vnc.html
index 84470de..8f65427 100644
--- a/vnc.html
+++ b/vnc.html
@@ -110,8 +110,7 @@
     </div>

     <!-- noVNC control bar -->
-    <div id="noVNC_control_bar_anchor" class="noVNC_vcenter">
-
+    <div id="noVNC_control_bar_anchor" class="noVNC_crosscenter">
         <div id="noVNC_control_bar">
             <div id="noVNC_control_bar_handle" title="Hide/Show the control bar"><div></div></div>

@@ -137,7 +136,7 @@
             <input type="image" alt="Extra keys" src="app/images/toggleextrakeys.svg"
                 id="noVNC_toggle_extra_keys_button" class="noVNC_button"
                 title="Show extra keys">
-            <div class="noVNC_vcenter">
+            <div class="noVNC_crosscenter">
             <div id="noVNC_modifiers" class="noVNC_panel">
                 <input type="image" alt="Ctrl" src="app/images/ctrl.svg"
                     id="noVNC_toggle_ctrl_button" class="noVNC_button"
@@ -164,7 +163,7 @@
             <input type="image" alt="Shutdown/Reboot" src="app/images/power.svg"
                 id="noVNC_power_button" class="noVNC_button"
                 title="Shutdown/Reboot...">
-            <div class="noVNC_vcenter">
+            <div class="noVNC_crosscenter">
             <div id="noVNC_power" class="noVNC_panel">
                 <div class="noVNC_heading">
                     <img alt="" src="app/images/power.svg"> Power
@@ -179,7 +178,7 @@
             <input type="image" alt="Clipboard" src="app/images/clipboard.svg"
                 id="noVNC_clipboard_button" class="noVNC_button"
                 title="Clipboard">
-            <div class="noVNC_vcenter">
+            <div class="noVNC_crosscenter">
             <div id="noVNC_clipboard" class="noVNC_panel">
                 <div class="noVNC_heading">
                     <img alt="" src="app/images/clipboard.svg"> Clipboard
@@ -200,7 +199,7 @@
             <input type="image" alt="Settings" src="app/images/settings.svg"
                 id="noVNC_settings_button" class="noVNC_button"
                 title="Settings">
-            <div class="noVNC_vcenter">
+            <div class="noVNC_crosscenter">
             <div id="noVNC_settings" class="noVNC_panel">
                 <div class="noVNC_heading">
                     <img alt="" src="app/images/settings.svg"> Settings
@@ -333,8 +332,20 @@

     </div> <!-- End of noVNC_control_bar -->

-    <div id="noVNC_hint_anchor" class="noVNC_vcenter">
-        <div id="noVNC_control_bar_hint">
+    <div class="noVNC_hint_anchor noVNC_crosscenter">
+        <div class="noVNC_control_bar_hint">
+        </div>
+    </div>
+    <div class="noVNC_hint_anchor noVNC_right noVNC_crosscenter">
+        <div class="noVNC_control_bar_hint">
+        </div>
+    </div>
+    <div class="noVNC_hint_anchor noVNC_top noVNC_crosscenter">
+        <div class="noVNC_control_bar_hint">
+        </div>
+    </div>
+    <div class="noVNC_hint_anchor noVNC_bottom noVNC_crosscenter">
+        <div class="noVNC_control_bar_hint">
         </div>
     </div>