"use strict"; /* * Copyright (c) 2013-2025 Vanessa Freudenberg * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ Object.extend(Squeak.Primitives.prototype, 'display', { initDisplay: function(display) { this.display = display; this.display.vm = this.vm; this.indexedColors = [ 0xFFFFFFFF, 0xFF000001, 0xFFFFFFFF, 0xFF808080, 0xFFFF0000, 0xFF00FF00, 0xFF0000FF, 0xFF00FFFF, 0xFFFFFF00, 0xFFFF00FF, 0xFF202020, 0xFF404040, 0xFF606060, 0xFF9F9F9F, 0xFFBFBFBF, 0xFFDFDFDF, 0xFF080808, 0xFF101010, 0xFF181818, 0xFF282828, 0xFF303030, 0xFF383838, 0xFF484848, 0xFF505050, 0xFF585858, 0xFF686868, 0xFF707070, 0xFF787878, 0xFF878787, 0xFF8F8F8F, 0xFF979797, 0xFFA7A7A7, 0xFFAFAFAF, 0xFFB7B7B7, 0xFFC7C7C7, 0xFFCFCFCF, 0xFFD7D7D7, 0xFFE7E7E7, 0xFFEFEFEF, 0xFFF7F7F7, 0xFF000001, 0xFF003300, 0xFF006600, 0xFF009900, 0xFF00CC00, 0xFF00FF00, 0xFF000033, 0xFF003333, 0xFF006633, 0xFF009933, 0xFF00CC33, 0xFF00FF33, 0xFF000066, 0xFF003366, 0xFF006666, 0xFF009966, 0xFF00CC66, 0xFF00FF66, 0xFF000099, 0xFF003399, 0xFF006699, 0xFF009999, 0xFF00CC99, 0xFF00FF99, 0xFF0000CC, 0xFF0033CC, 0xFF0066CC, 0xFF0099CC, 0xFF00CCCC, 0xFF00FFCC, 0xFF0000FF, 0xFF0033FF, 0xFF0066FF, 0xFF0099FF, 0xFF00CCFF, 0xFF00FFFF, 0xFF330000, 0xFF333300, 0xFF336600, 0xFF339900, 0xFF33CC00, 0xFF33FF00, 0xFF330033, 0xFF333333, 0xFF336633, 0xFF339933, 0xFF33CC33, 0xFF33FF33, 0xFF330066, 0xFF333366, 0xFF336666, 0xFF339966, 0xFF33CC66, 0xFF33FF66, 0xFF330099, 0xFF333399, 0xFF336699, 0xFF339999, 0xFF33CC99, 0xFF33FF99, 0xFF3300CC, 0xFF3333CC, 0xFF3366CC, 0xFF3399CC, 0xFF33CCCC, 0xFF33FFCC, 0xFF3300FF, 0xFF3333FF, 0xFF3366FF, 0xFF3399FF, 0xFF33CCFF, 0xFF33FFFF, 0xFF660000, 0xFF663300, 0xFF666600, 0xFF669900, 0xFF66CC00, 0xFF66FF00, 0xFF660033, 0xFF663333, 0xFF666633, 0xFF669933, 0xFF66CC33, 0xFF66FF33, 0xFF660066, 0xFF663366, 0xFF666666, 0xFF669966, 0xFF66CC66, 0xFF66FF66, 0xFF660099, 0xFF663399, 0xFF666699, 0xFF669999, 0xFF66CC99, 0xFF66FF99, 0xFF6600CC, 0xFF6633CC, 0xFF6666CC, 0xFF6699CC, 0xFF66CCCC, 0xFF66FFCC, 0xFF6600FF, 0xFF6633FF, 0xFF6666FF, 0xFF6699FF, 0xFF66CCFF, 0xFF66FFFF, 0xFF990000, 0xFF993300, 0xFF996600, 0xFF999900, 0xFF99CC00, 0xFF99FF00, 0xFF990033, 0xFF993333, 0xFF996633, 0xFF999933, 0xFF99CC33, 0xFF99FF33, 0xFF990066, 0xFF993366, 0xFF996666, 0xFF999966, 0xFF99CC66, 0xFF99FF66, 0xFF990099, 0xFF993399, 0xFF996699, 0xFF999999, 0xFF99CC99, 0xFF99FF99, 0xFF9900CC, 0xFF9933CC, 0xFF9966CC, 0xFF9999CC, 0xFF99CCCC, 0xFF99FFCC, 0xFF9900FF, 0xFF9933FF, 0xFF9966FF, 0xFF9999FF, 0xFF99CCFF, 0xFF99FFFF, 0xFFCC0000, 0xFFCC3300, 0xFFCC6600, 0xFFCC9900, 0xFFCCCC00, 0xFFCCFF00, 0xFFCC0033, 0xFFCC3333, 0xFFCC6633, 0xFFCC9933, 0xFFCCCC33, 0xFFCCFF33, 0xFFCC0066, 0xFFCC3366, 0xFFCC6666, 0xFFCC9966, 0xFFCCCC66, 0xFFCCFF66, 0xFFCC0099, 0xFFCC3399, 0xFFCC6699, 0xFFCC9999, 0xFFCCCC99, 0xFFCCFF99, 0xFFCC00CC, 0xFFCC33CC, 0xFFCC66CC, 0xFFCC99CC, 0xFFCCCCCC, 0xFFCCFFCC, 0xFFCC00FF, 0xFFCC33FF, 0xFFCC66FF, 0xFFCC99FF, 0xFFCCCCFF, 0xFFCCFFFF, 0xFFFF0000, 0xFFFF3300, 0xFFFF6600, 0xFFFF9900, 0xFFFFCC00, 0xFFFFFF00, 0xFFFF0033, 0xFFFF3333, 0xFFFF6633, 0xFFFF9933, 0xFFFFCC33, 0xFFFFFF33, 0xFFFF0066, 0xFFFF3366, 0xFFFF6666, 0xFFFF9966, 0xFFFFCC66, 0xFFFFFF66, 0xFFFF0099, 0xFFFF3399, 0xFFFF6699, 0xFFFF9999, 0xFFFFCC99, 0xFFFFFF99, 0xFFFF00CC, 0xFFFF33CC, 0xFFFF66CC, 0xFFFF99CC, 0xFFFFCCCC, 0xFFFFFFCC, 0xFFFF00FF, 0xFFFF33FF, 0xFFFF66FF, 0xFFFF99FF, 0xFFFFCCFF, 0xFFFFFFFF]; }, primitiveBeCursor: function(argCount) { if (this.display.cursorCanvas) { var cursorForm = this.loadForm(this.stackNonInteger(argCount), true), maskForm = argCount === 1 ? this.loadForm(this.stackNonInteger(0)) : null; if (!this.success || !cursorForm) return false; var cursorCanvas = this.display.cursorCanvas, context = cursorCanvas.getContext("2d"), bounds = {left: 0, top: 0, right: cursorForm.width, bottom: cursorForm.height}; cursorCanvas.width = cursorForm.width; cursorCanvas.height = cursorForm.height; if (cursorForm.depth === 1) { if (maskForm) { cursorForm = this.cursorMergeMask(cursorForm, maskForm); this.showForm(context, cursorForm, bounds, [0x00000000, 0xFF0000FF, 0xFFFFFFFF, 0xFF000000]); } else { this.showForm(context, cursorForm, bounds, [0x00000000, 0xFF000000]); } } else { this.showForm(context, cursorForm, bounds, true); } var scale = this.display.scale || 1.0; if (cursorForm.width <= 16 && cursorForm.height <= 16) { scale = 1.0; } cursorCanvas.style.width = Math.round(cursorCanvas.width * scale) + "px"; cursorCanvas.style.height = Math.round(cursorCanvas.height * scale) + "px"; this.display.cursorOffsetX = cursorForm.offsetX * scale|0; this.display.cursorOffsetY = cursorForm.offsetY * scale|0; } this.vm.popN(argCount); return true; }, cursorMergeMask: function(cursor, mask) { // make 2-bit form from cursor and mask 1-bit forms var bits = new Uint32Array(16); for (var y = 0; y < 16; y++) { var c = cursor.bits[y], m = mask.bits[y], bit = 0x80000000, merged = 0; for (var x = 0; x < 16; x++) { merged = merged | ((m & bit) >> x) | ((c & bit) >> (x + 1)); bit = bit >>> 1; } bits[y] = merged; } return { obj: cursor.obj, bits: bits, depth: 2, width: 16, height: 16, offsetX: cursor.offsetX, offsetY: cursor.offsetY, msb: true, pixPerWord: 16, pitch: 1, } }, primitiveBeDisplay: function(argCount) { var displayObj = this.vm.stackValue(0); this.vm.specialObjects[Squeak.splOb_TheDisplay] = displayObj; this.vm.popN(argCount); // return self return true; }, primitiveReverseDisplay: function(argCount) { this.reverseDisplay = !this.reverseDisplay; this.redrawDisplay(); if (this.display.cursorCanvas) { var canvas = this.display.cursorCanvas, context = canvas.getContext("2d"), image = context.getImageData(0, 0, canvas.width, canvas.height), data = new Uint32Array(image.data.buffer); for (var i = 0; i < data.length; i++) data[i] = data[i] ^ 0x00FFFFFF; context.putImageData(image, 0, 0); } return true; }, primitiveShowDisplayRect: function(argCount) { // Force the given rectangular section of the Display to be copied to the screen. var rect = { left: this.stackInteger(3), top: this.stackInteger(1), right: this.stackInteger(2), bottom: this.stackInteger(0), }; if (!this.success) return false; this.redrawDisplay(rect); this.vm.popN(argCount); return true; }, redrawDisplay: function(rect) { var theDisplay = this.theDisplay(), bounds = {left: 0, top: 0, right: theDisplay.width, bottom: theDisplay.height}; if (rect) { if (rect.left > bounds.left) bounds.left = rect.left; if (rect.right < bounds.right) bounds.right = rect.right; if (rect.top > bounds.top) bounds.top = rect.top; if (rect.bottom < bounds.bottom) bounds.bottom = rect.bottom; } if (bounds.left < bounds.right && bounds.top < bounds.bottom) this.displayUpdate(theDisplay, bounds); }, showForm: function(ctx, form, rect, cursorColors) { if (!rect) return; var srcX = rect.left, srcY = rect.top, srcW = rect.right - srcX, srcH = rect.bottom - srcY, pixels = ctx.createImageData(srcW, srcH), pixelData = pixels.data; if (!pixelData.buffer) { // mobile IE uses a different data-structure pixelData = new Uint8Array(srcW * srcH * 4); } var dest = new Uint32Array(pixelData.buffer); switch (form.depth) { case 1: case 2: case 4: case 8: var colors = cursorColors || this.swappedColors; if (!colors) { colors = []; for (var i = 0; i < 256; i++) { var argb = this.indexedColors[i], abgr = (argb & 0xFF00FF00) // green and alpha + ((argb & 0x00FF0000) >> 16) // shift red down + ((argb & 0x000000FF) << 16); // shift blue up colors[i] = abgr; } this.swappedColors = colors; } if (this.reverseDisplay) { if (cursorColors) { colors = cursorColors.map(function(c){return c ^ 0x00FFFFFF}); } else { if (!this.reversedColors) this.reversedColors = colors.map(function(c){return c ^ 0x00FFFFFF}); colors = this.reversedColors; } } var mask = (1 << form.depth) - 1; var leftSrcShift = 32 - (srcX % form.pixPerWord + 1) * form.depth; for (var y = 0; y < srcH; y++) { var srcIndex = form.pitch * srcY + (srcX / form.pixPerWord | 0); var srcShift = leftSrcShift; var src = form.bits[srcIndex]; var dstIndex = pixels.width * y; for (var x = 0; x < srcW; x++) { dest[dstIndex++] = colors[(src >>> srcShift) & mask]; if ((srcShift -= form.depth) < 0) { srcShift = 32 - form.depth; src = form.bits[++srcIndex]; } } srcY++; }; break; case 16: var leftSrcShift = srcX % 2 ? 0 : 16; for (var y = 0; y < srcH; y++) { var srcIndex = form.pitch * srcY + (srcX / 2 | 0); var srcShift = leftSrcShift; var src = form.bits[srcIndex]; var dstIndex = pixels.width * y; for (var x = 0; x < srcW; x++) { var rgb = src >>> srcShift; dest[dstIndex++] = ((rgb & 0x7C00) >> 7) // shift red down 2*5, up 0*8 + 3 + ((rgb & 0x03E0) << 6) // shift green down 1*5, up 1*8 + 3 + ((rgb & 0x001F) << 19) // shift blue down 0*5, up 2*8 + 3 + 0xFF000000; // set alpha to opaque if ((srcShift -= 16) < 0) { srcShift = 16; src = form.bits[++srcIndex]; } } srcY++; }; break; case 32: var opaque = cursorColors ? 0 : 0xFF000000; // keep alpha for cursors for (var y = 0; y < srcH; y++) { var srcIndex = form.pitch * srcY + srcX; var dstIndex = pixels.width * y; for (var x = 0; x < srcW; x++) { var argb = form.bits[srcIndex++]; // convert ARGB -> ABGR var abgr = (argb & 0xFF00FF00) // green and alpha is okay | ((argb & 0x00FF0000) >> 16) // shift red down | ((argb & 0x000000FF) << 16) // shift blue up | opaque; // set alpha to opaque dest[dstIndex++] = abgr; } srcY++; }; break; default: throw Error("depth not implemented"); }; if (pixels.data !== pixelData) { pixels.data.set(pixelData); } ctx.putImageData(pixels, rect.left, rect.top); }, primitiveDeferDisplayUpdates: function(argCount) { var flag = this.stackBoolean(0); if (!this.success) return false; this.deferDisplayUpdates = flag; this.vm.popN(argCount); return true; }, primitiveForceDisplayUpdate: function(argCount) { this.vm.breakOut(); // show on screen this.vm.popN(argCount); return true; }, primitiveScanCharacters: function(argCount) { if (argCount !== 6) return false; // Load the arguments var kernDelta = this.stackInteger(0); var stops = this.stackNonInteger(1); var scanRightX = this.stackInteger(2); var sourceString = this.stackNonInteger(3); var scanStopIndex = this.stackInteger(4); var scanStartIndex = this.stackInteger(5); if (!this.success) return false; if (stops.pointersSize() < 258 || !sourceString.isBytes()) return false; if (!(scanStartIndex > 0 && scanStopIndex > 0 && scanStopIndex <= sourceString.bytesSize())) return false; // Load receiver and required instVars var rcvr = this.stackNonInteger(6); if (!this.success || rcvr.pointersSize() < 4) return false; var scanDestX = this.checkSmallInt(rcvr.pointers[0]); var scanLastIndex = this.checkSmallInt(rcvr.pointers[1]); var scanXTable = this.checkNonInteger(rcvr.pointers[2]); var scanMap = this.checkNonInteger(rcvr.pointers[3]); if (!this.success || scanMap.pointersSize() !== 256) return false; var maxGlyph = scanXTable.pointersSize() - 2; // Okay, here we go. We have eliminated nearly all failure // conditions, to optimize the inner fetches. var EndOfRun = 257; var CrossedX = 258; var scanLastIndex = scanStartIndex; while (scanLastIndex <= scanStopIndex) { // Known to be okay since scanStartIndex > 0 and scanStopIndex <= sourceString size var ascii = sourceString.bytes[scanLastIndex - 1]; // Known to be okay since stops size >= 258 var stopReason = stops.pointers[ascii]; if (!stopReason.isNil) { // Store everything back and get out of here since some stop conditionn needs to be checked" this.ensureSmallInt(scanDestX); if (!this.success) return false; rcvr.pointers[0] = scanDestX; rcvr.pointers[1] = scanLastIndex; return this.popNandPushIfOK(7, stopReason); } // Known to be okay since scanMap size = 256 var glyphIndex = this.checkSmallInt(scanMap.pointers[ascii]); // fail if the glyphIndex is out of range if (!this.success || glyphIndex < 0 || glyphIndex > maxGlyph) return false; var sourceX = this.checkSmallInt(scanXTable.pointers[glyphIndex]); var sourceX2 = this.checkSmallInt(scanXTable.pointers[glyphIndex + 1]); // Above may fail if non-integer entries in scanXTable if (!this.success) return false; var nextDestX = scanDestX + sourceX2 - sourceX; if (nextDestX > scanRightX) { // Store everything back and get out of here since we got to the right edge this.ensureSmallInt(scanDestX); if (!this.success) return false; rcvr.pointers[0] = scanDestX; rcvr.pointers[1] = scanLastIndex; stopReason = stops.pointers[CrossedX - 1]; return this.popNandPushIfOK(7, stopReason); } scanDestX = nextDestX + kernDelta; scanLastIndex = scanLastIndex + 1; } this.ensureSmallInt(scanDestX); if (!this.success) return false; rcvr.pointers[0] = scanDestX; rcvr.pointers[1] = scanStopIndex; stopReason = stops.pointers[EndOfRun - 1]; return this.popNandPushIfOK(7, stopReason); }, primitiveScreenSize: function(argCount) { var display = this.display, w = display.width || display.context.canvas.width, h = display.height || display.context.canvas.height; return this.popNandPushIfOK(argCount+1, this.makePointWithXandY(w, h)); }, primitiveScreenScaleFactor: function(argCount) { var scaleFactor = (this.display.highdpi ? window.devicePixelRatio : 1) || 1; return this.popNandPushIfOK(argCount+1, scaleFactor); }, primitiveSetFullScreen: function(argCount) { var flag = this.stackBoolean(0); if (!this.success) return false; if (this.display.fullscreen != flag) { if (this.display.fullscreenRequest) { // freeze until we get the right display size var unfreeze = this.vm.freeze(); this.display.fullscreenRequest(flag, function thenDo() { unfreeze(); }); } else { this.display.fullscreen = flag; this.vm.breakOut(); // let VM go into fullscreen mode } } this.vm.popN(argCount); return true; }, primitiveTestDisplayDepth: function(argCount) { var supportedDepths = [1, 2, 4, 8, 16, 32]; // match showForm return this.popNandPushBoolIfOK(argCount+1, supportedDepths.indexOf(this.stackInteger(0)) >= 0); }, loadForm: function(formObj, withOffset) { if (formObj.isNil) return null; var form = { obj: formObj, bits: formObj.pointers[Squeak.Form_bits].wordsOrBytes(), depth: formObj.pointers[Squeak.Form_depth], width: formObj.pointers[Squeak.Form_width], height: formObj.pointers[Squeak.Form_height], } if (withOffset) { var offset = formObj.pointers[Squeak.Form_offset]; form.offsetX = offset.pointers ? offset.pointers[Squeak.Point_x] : 0; form.offsetY = offset.pointers ? offset.pointers[Squeak.Point_y] : 0; } if (form.width === 0 || form.height === 0) return form; if (!(form.width > 0 && form.height > 0)) return null; form.msb = form.depth > 0; if (!form.msb) form.depth = -form.depth; if (!(form.depth > 0)) return null; // happens if not int form.pixPerWord = 32 / form.depth; form.pitch = (form.width + (form.pixPerWord - 1)) / form.pixPerWord | 0; if (form.bits.length !== (form.pitch * form.height)) { if (form.bits.length > (form.pitch * form.height)) { this.vm.warnOnce("loadForm(): " + form.bits.length + " !== " + form.pitch + "*" + form.height + "=" + (form.pitch*form.height)); } else { return null; } } return form; }, theDisplay: function() { return this.loadForm(this.vm.specialObjects[Squeak.splOb_TheDisplay]); }, displayDirty: function(form, rect) { if (!this.deferDisplayUpdates && form == this.vm.specialObjects[Squeak.splOb_TheDisplay]) this.displayUpdate(this.theDisplay(), rect); }, displayUpdate: function(form, rect) { this.showForm(this.display.context, form, rect); this.display.lastTick = this.vm.lastTick; this.display.idle = 0; }, primitiveBeep: function(argCount) { var ctx = Squeak.startAudioOut(); if (ctx) { var beep = ctx.createOscillator(); beep.connect(ctx.destination); beep.type = 'square'; beep.frequency.value = 880; beep.start(); beep.stop(ctx.currentTime + 0.05); } else { this.vm.warnOnce("could not initialize audio"); } return this.popNIfOK(argCount); }, hostWindow_primitiveHostWindowTitle: function(argCount) { if (this.display.setTitle) { var utf8 = this.stackNonInteger(0).bytes; var title = (new TextDecoder()).decode(utf8); this.display.setTitle(title); } return this.popNIfOK(argCount); } });