Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions app/styles/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -1396,18 +1396,31 @@ a:visited {
border: 1px solid #e5e5e5;
line-height: 1;
}
#noVNC_setting_enable_hidpi_option {
display: none!important;
}
#noVNC_setting_enable_hidpi_option.show {
display: flex!important;
}
#noVNC_auto_placement_option {
display: none!important;
}
#noVNC_auto_placement_option.show {
display: flex!important;
}
.monitor-resolution-scales {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.monitor-resolution-scales label {
align-items: center;
background-color: #f1f1f1;
border: 1px solid #e5e5e5;
border-radius: 5px;
display: flex;
font-size: 13px;
gap: 6px;
line-height: 1;
padding: 4px 7px;
}
.monitor-resolution-scales select {
font-size: 13px;
}
#noVNC_refreshMonitors {
position: absolute;
top: 20px;
Expand Down
121 changes: 108 additions & 13 deletions app/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ import { perfLogger } from '../core/util/performance-logger.js';
// perfLogger.enable(5000);

const PAGE_TITLE = "KasmVNC";
const RESOLUTION_SCALE_MIN = 25;
const RESOLUTION_SCALE_MAX = 200;
const RESOLUTION_SCALE_STEP = 25;

var currentEventCount = -1;
var idleCounter = 0;
Expand Down Expand Up @@ -86,6 +89,10 @@ const UI = {
displayWindows: new Map([['primary', 'primary']]),
registeredWindows: new Map([['primary', 'primary']]),
fpsChartTicks: [],
resolutionScaleOptions: Array.from(
{ length: ((RESOLUTION_SCALE_MAX - RESOLUTION_SCALE_MIN) / RESOLUTION_SCALE_STEP) + 1 },
(_, i) => RESOLUTION_SCALE_MIN + (i * RESOLUTION_SCALE_STEP)
),

monitorDragOk: false,
monitorStartX: 0,
Expand Down Expand Up @@ -603,6 +610,7 @@ const UI = {
// unless the optional parameter changeFunc is used instead.
addSettingChangeHandler(name, changeFunc) {
const settingElem = document.getElementById("noVNC_setting_" + name);
if (!settingElem) return;
if (changeFunc === undefined) {
changeFunc = () => UI.saveSetting(name);
}
Expand Down Expand Up @@ -2441,6 +2449,11 @@ const UI = {
},

closeDisplays() {
UI.syncMonitorResolutionScaleControls();
if (UI.rfb && UI.monitors.length > 0) {
UI.setScreenPlan();
UI.draw();
}
document.getElementById('noVNC_displays').classList.remove("noVNC_open");
},

Expand Down Expand Up @@ -2513,31 +2526,32 @@ const UI = {
initMonitors(screenPlan) {
const { scale } = UI.multiMonitorSettings()
let monitors = []
let showNativeResolution = false
let num = 1;
screenPlan.screens.forEach(screen => {
if (parseFloat(screen.pixelRatio) != 1) {
showNativeResolution = true
}
const resolutionScale = UI.normalizeResolutionScale(
screen.resolutionScale || UI.defaultResolutionScale(screen.pixelRatio));
const localWidth = screen.containerWidth ||
Math.round(screen.serverWidth / UI.resolutionScaleRatio(resolutionScale, screen.pixelRatio));
const localHeight = screen.containerHeight ||
Math.round(screen.serverHeight / UI.resolutionScaleRatio(resolutionScale, screen.pixelRatio));
const scaleRatio = UI.resolutionScaleRatio(resolutionScale, screen.pixelRatio);
monitors.push({
id: screen.screenID,
x: screen.x / scale,
y: screen.y / scale,
w: screen.serverWidth / scale,
h: screen.serverHeight / scale,
w: (localWidth * scaleRatio) / scale,
h: (localHeight * scaleRatio) / scale,
localWidth,
localHeight,
pixelRatio: screen.pixelRatio,
resolutionScale,
scale: 1,
fill: '#eeeeeecc',
isDragging: false,
num
})
num++
})
if (showNativeResolution) {
document.getElementById('noVNC_setting_enable_hidpi_option').classList.add("show");
} else {
document.getElementById('noVNC_setting_enable_hidpi_option').classList.remove("show");
}
UI.monitors = monitors
let deepCopyMonitors = JSON.parse(JSON.stringify(monitors))
UI.sortedMonitors = deepCopyMonitors.sort((a, b) => {
Expand All @@ -2546,7 +2560,80 @@ const UI = {
}
return a.x - b.x
})
UI.renderResolutionScaleControls()

},

normalizeResolutionScale(value) {
value = parseInt(value);
if (!Number.isFinite(value)) {
return 100;
}
value = Math.round(value / RESOLUTION_SCALE_STEP) * RESOLUTION_SCALE_STEP;
return Math.min(RESOLUTION_SCALE_MAX, Math.max(RESOLUTION_SCALE_MIN, value));
},

defaultResolutionScale(pixelRatio) {
return UI.normalizeResolutionScale((parseFloat(pixelRatio) || 1) * 100);
},

resolutionScaleRatio(resolutionScale, pixelRatio) {
return (parseFloat(pixelRatio) || 1) / (UI.normalizeResolutionScale(resolutionScale) / 100);
},

renderResolutionScaleControls() {
const container = document.getElementById('noVNC_monitor_resolution_scales');
if (!container) return;

container.replaceChildren();
UI.sortedMonitors.forEach(monitor => {
const label = document.createElement('label');
label.textContent = `Display ${monitor.num}`;

const select = document.createElement('select');
select.dataset.screenId = monitor.id;
UI.resolutionScaleOptions.forEach(value => {
const option = document.createElement('option');
option.value = value;
option.textContent = `${value}%`;
select.appendChild(option);
});
select.value = String(UI.normalizeResolutionScale(monitor.resolutionScale));
select.addEventListener('change', UI.updateMonitorResolutionScale);

label.appendChild(select);
container.appendChild(label);
});
},

updateMonitorResolutionScale(event) {
const screenID = event.target.dataset.screenId;
const resolutionScale = UI.normalizeResolutionScale(event.target.value);
UI.updateMonitorResolutionScaleValue(screenID, resolutionScale);
UI.recenter();
UI.draw();
},

updateMonitorResolutionScaleValue(screenID, resolutionScale) {
const updateMonitor = monitor => {
if (String(monitor.id) !== String(screenID)) return;

monitor.resolutionScale = resolutionScale;
const scaleRatio = UI.resolutionScaleRatio(monitor.resolutionScale, monitor.pixelRatio);
monitor.w = (monitor.localWidth * scaleRatio) / UI.multiMonitorSettings().scale;
monitor.h = (monitor.localHeight * scaleRatio) / UI.multiMonitorSettings().scale;
};

UI.monitors.forEach(updateMonitor);
UI.sortedMonitors.forEach(updateMonitor);
},

syncMonitorResolutionScaleControls() {
document.querySelectorAll('#noVNC_monitor_resolution_scales select').forEach(select => {
UI.updateMonitorResolutionScaleValue(
select.dataset.screenId,
UI.normalizeResolutionScale(select.value));
});
},

updateMonitors(screenPlan) {
Expand Down Expand Up @@ -2782,10 +2869,17 @@ const UI = {
for (var i = 0; i < monitors.length; i++) {
var monitor = monitors[i];
var a = sortedMonitors.find(el => el.id === monitor.id)
const serverWidth = Math.round(a.w * scale);
const serverHeight = Math.round(a.h * scale);
const displayScale = 1 / UI.resolutionScaleRatio(a.resolutionScale, a.pixelRatio);
screens.push({
screenID: a.id,
serverHeight: Math.round(a.h * scale),
serverWidth: Math.round(a.w * scale),
width: Math.round(a.localWidth),
height: Math.round(a.localHeight),
serverHeight,
serverWidth,
scale: displayScale,
resolutionScale: a.resolutionScale,
x: Math.round((a.x - left) * scale),
y: Math.round((a.y - top) * scale)
})
Expand All @@ -2797,6 +2891,7 @@ const UI = {
}
if (UI.rfb) {
UI.rfb.applyScreenPlan(screenPlan);
window.setTimeout(() => UI.rfb.updateConnectionSettings(), 0);
}
},

Expand Down
87 changes: 72 additions & 15 deletions core/display.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,27 @@ import {Canvas2DRenderer} from "./renderers/Canvas2DRenderer";
import {WebGLRenderer} from "./renderers/WebGLRenderer";
import { perfLogger } from './util/performance-logger.js';

const MIN_RESOLUTION_SCALE = 25;
const MAX_RESOLUTION_SCALE = 200;
const RESOLUTION_SCALE_STEP = 25;

function clampResolutionScale(value) {
value = Number(value);
if (!Number.isFinite(value)) {
return 100;
}
value = Math.round(value / RESOLUTION_SCALE_STEP) * RESOLUTION_SCALE_STEP;
return Math.min(MAX_RESOLUTION_SCALE, Math.max(MIN_RESOLUTION_SCALE, value));
}

function defaultResolutionScale(pixelRatio) {
return clampResolutionScale((Number(pixelRatio) || 1) * 100);
}

function resolutionScaleRatio(resolutionScale, pixelRatio) {
return (Number(pixelRatio) || 1) / (clampResolutionScale(resolutionScale) / 100);
}

export default class Display {
constructor(target, rfb, isPrimaryDisplay, videoRenderingMode = 'canvas2d') {
Log.Debug(">> Display.constructor");
Expand Down Expand Up @@ -156,6 +177,8 @@ export default class Display {
relativePositionX: 0, //offset relative to primary monitor, always 0 for primary
relativePositionY: 0, //offset relative to primary monitor, always 0 for primary
pixelRatio: window.devicePixelRatio,
resolutionScale: defaultResolutionScale(window.devicePixelRatio),
resolutionScaleCustom: false,
containerHeight: this._target.parentNode.offsetHeight,
containerWidth: this._target.parentNode.offsetWidth,
channel: null,
Expand Down Expand Up @@ -299,6 +322,9 @@ export default class Display {
this._screens[i].containerHeight = Math.floor(parentNodeSize.height / 2) * 2;
this._screens[i].containerWidth = Math.floor(parentNodeSize.width / 2) * 2;
this._screens[i].pixelRatio = window.devicePixelRatio;
if (!this._screens[i].resolutionScaleCustom) {
this._screens[i].resolutionScale = defaultResolutionScale(this._screens[i].pixelRatio);
}
this._screens[i].width = this._screens[i].containerWidth;
this._screens[i].height = this._screens[i].containerHeight;

Expand All @@ -310,15 +336,21 @@ export default class Display {
//max the resolution of a single screen to 1280
if (
(this._screens[i].serverReportedWidth > 0 && this._screens[i].serverReportedHeight > 0) &&
(
disableScaling ||
(this._screens[i].serverReportedWidth !== this._screens[i].serverWidth || this._screens[i].serverReportedHeight !== this._screens[i].serverHeight)
) &&
(disableScaling ||
(!this._screens[i].resolutionScaleCustom &&
(this._screens[i].serverReportedWidth !== this._screens[i].serverWidth ||
this._screens[i].serverReportedHeight !== this._screens[i].serverHeight))) &&
(!max_width && !max_height)
) {
height = this._screens[i].serverReportedHeight;
width = this._screens[i].serverReportedWidth;
}
else if (!max_width && !max_height) {
const scaleRatio = resolutionScaleRatio(this._screens[i].resolutionScale, this._screens[i].pixelRatio);
width = Math.floor(width * scaleRatio);
height = Math.floor(height * scaleRatio);
scale = 1 / scaleRatio;
}
else if (width > 1280 && !disableLimit && resolutionQuality == 1 && streamMode == encodings.pseudoEncodingStreamingModeJpegWebp) {
height = Math.floor(1280 * (height/width)); //keeping the aspect ratio of original resolution, shrink y to match x
width = 1280;
Expand Down Expand Up @@ -384,19 +416,39 @@ export default class Display {
for (let i = 0; i < screenPlan.screens.length; i++) {
for (let z = 0; z < this._screens.length; z++) {
if (screenPlan.screens[i].screenID === this._screens[z].screenID) {
if (this._screens[z].x !== screenPlan.screens[i].x || this._screens[z].y !== screenPlan.screens[i].y) {
if (z == 0) {
this._screens[z].x = screenPlan.screens[i].x;
this._screens[z].y = screenPlan.screens[i].y;
if (typeof screenPlan.screens[i].resolutionScale !== 'undefined') {
const resolutionScale = clampResolutionScale(screenPlan.screens[i].resolutionScale);
if (this._screens[z].resolutionScale !== resolutionScale) {
this._screens[z].resolutionScale = resolutionScale;
this._screens[z].resolutionScaleCustom = true;
changes = true;
}
}
if (typeof screenPlan.screens[i].serverWidth !== 'undefined' &&
typeof screenPlan.screens[i].serverHeight !== 'undefined' &&
(this._screens[z].serverWidth !== screenPlan.screens[i].serverWidth ||
this._screens[z].serverHeight !== screenPlan.screens[i].serverHeight)) {
this._screens[z].serverWidth = screenPlan.screens[i].serverWidth;
this._screens[z].serverHeight = screenPlan.screens[i].serverHeight;
changes = true;
}
if (typeof screenPlan.screens[i].width !== 'undefined') {
this._screens[z].width = screenPlan.screens[i].width;
}
if (typeof screenPlan.screens[i].height !== 'undefined') {
this._screens[z].height = screenPlan.screens[i].height;
}
if (typeof screenPlan.screens[i].scale !== 'undefined') {
this._screens[z].scale = screenPlan.screens[i].scale;
}
if (this._screens[z].x !== screenPlan.screens[i].x || this._screens[z].y !== screenPlan.screens[i].y) {
this._screens[z].x = screenPlan.screens[i].x;
this._screens[z].y = screenPlan.screens[i].y;
changes = true;
}
if (this._screens[z].x2 !== this._screens[z].x + this._screens[z].serverWidth || this._screens[z].y2 !== this._screens[z].y + this._screens[z].serverHeight) {
if (z == 0) {
this._screens[z].x2 = this._screens[z].x + this._screens[z].serverWidth
this._screens[z].y2 = this._screens[z].y + this._screens[z].serverHeight

}
this._screens[z].x2 = this._screens[z].x + this._screens[z].serverWidth
this._screens[z].y2 = this._screens[z].y + this._screens[z].serverHeight
changes = true;
}
}
Expand All @@ -405,7 +457,7 @@ export default class Display {
return changes;
}

addScreen(screenID, width, height, pixelRatio, containerHeight, containerWidth, scale, serverWidth, serverHeight, x, y, windowId) {
addScreen(screenID, width, height, pixelRatio, containerHeight, containerWidth, scale, serverWidth, serverHeight, x, y, windowId, resolutionScale = null) {
if (!this._isPrimaryDisplay) {
throw new Error("Cannot add a screen to a secondary display.");
}
Expand All @@ -424,14 +476,17 @@ export default class Display {
if (screenIdx > 0) {
//existing screen, update
const existing_screen = this._screens[screenIdx];
const resolvedResolutionScale = resolutionScale || defaultResolutionScale(pixelRatio);
if (existing_screen.serverHeight !== serverHeight || existing_screen.serverWidth !== serverWidth || existing_screen.width !== width || existing_screen.height !== height
|| existing_screen.containerHeight !== containerHeight || existing_screen.containerWidth !== containerWidth || existing_screen.scale !== scale || existing_screen.pixelRatio !== pixelRatio ||
existing_screen.x !== x || existing_screen.y !== y) {
existing_screen.x !== x || existing_screen.y !== y || existing_screen.resolutionScale !== resolvedResolutionScale) {
existing_screen.width = width;
existing_screen.height = height;
existing_screen.containerHeight = containerHeight;
existing_screen.containerWidth = containerWidth;
existing_screen.pixelRatio = pixelRatio;
existing_screen.resolutionScale = resolvedResolutionScale;
existing_screen.resolutionScaleCustom = resolutionScale !== null;
existing_screen.scale = scale;
existing_screen.serverWidth = serverWidth;
existing_screen.serverHeight = serverHeight;
Expand Down Expand Up @@ -459,6 +514,8 @@ export default class Display {
x: x,
y: 0,
pixelRatio: pixelRatio,
resolutionScale: resolutionScale || defaultResolutionScale(pixelRatio),
resolutionScaleCustom: resolutionScale !== null,
containerHeight: containerHeight,
containerWidth: containerWidth,
channel: new BroadcastChannel(`channel_${screenID}`),
Expand Down
Loading