├── .gitignore ├── .gitmodules ├── LICENSE.md ├── Makefile ├── README.md ├── glslx └── shaders.glslx ├── osx └── osx.mm ├── package.json ├── src ├── browser │ ├── canvas.sk │ ├── canvas2d.sk │ ├── canvaswebgl.sk │ ├── context.sk │ ├── fontinstance.sk │ ├── support.sk │ ├── typedarray.sk │ ├── webgl.sk │ └── window.sk ├── editor │ ├── action.sk │ ├── app.sk │ ├── buffer.sk │ ├── change.sk │ ├── diagnostic.sk │ ├── lexer.sk │ ├── model.sk │ ├── scrollbar.sk │ ├── selection.sk │ ├── shortcuts.sk │ ├── view.sk │ └── windowcontroller.sk ├── graphics │ ├── context.sk │ ├── dropshadow.sk │ ├── glyphbatch.sk │ ├── maskpacker.sk │ ├── rgba.sk │ ├── shaders.sk │ └── solidbatch.sk ├── support │ ├── build.sk │ ├── exports.sk │ ├── fixedarray.sk │ ├── frozenlist.sk │ ├── growablearray.sk │ ├── log.sk │ ├── rect.sk │ └── vector.sk ├── syntax │ └── skew.sk ├── themes │ ├── atom.sk │ ├── brackets.sk │ ├── earthsong.sk │ ├── idle.sk │ ├── monokai.sk │ ├── solarized.sk │ ├── twilight.sk │ ├── visualstudio.sk │ └── xcode.sk └── ui │ ├── color.sk │ ├── event.sk │ ├── font.sk │ ├── platform.sk │ ├── renderer.sk │ ├── theme.sk │ ├── view.sk │ └── window.sk ├── terminal └── terminal.cpp └── www └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | /osx/compiled.cpp 4 | /osx/Sky.app/ 5 | /terminal/compiled.cpp 6 | /terminal/sky 7 | /www/compiled.js 8 | /www/compiled.js.map 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "skew"] 2 | path = skew 3 | url = git@github.com:evanw/skew.git 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Evan Wallace 3 | 4 | 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: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | 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. 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SKEW = node_modules/.bin/skewc src/*/*.sk --message-limit=0 2 | GLSLX = node_modules/.bin/glslx glslx/shaders.glslx --format=skew --output=src/graphics/shaders.sk 3 | 4 | SKEW_FLAGS_JS += --output-file=www/compiled.js 5 | SKEW_FLAGS_JS += --define:BUILD=WWW 6 | 7 | SKEW_FLAGS_OSX += --output-file=osx/compiled.cpp 8 | SKEW_FLAGS_OSX += --define:BUILD=OSX 9 | 10 | SKEW_FLAGS_TERMINAL += --output-file=terminal/compiled.cpp 11 | SKEW_FLAGS_TERMINAL += --define:BUILD=TERMINAL 12 | 13 | INFO_PLIST_DATA = 'NSHighResolutionCapable' 14 | INFO_PLIST_PATH = osx/Sky.app/Contents/Info.plist 15 | OSX_APP_PATH = osx/Sky.app/Contents/MacOS/Sky 16 | 17 | CLANG_FLAGS += -I node_modules/skew 18 | CLANG_FLAGS += -std=c++11 19 | CLANG_FLAGS += -Wall 20 | CLANG_FLAGS += -Wextra 21 | CLANG_FLAGS += -Wno-switch 22 | CLANG_FLAGS += -Wno-unused-parameter 23 | 24 | CLANG_FLAGS_RELEASE += -DNDEBUG 25 | CLANG_FLAGS_RELEASE += -fomit-frame-pointer 26 | CLANG_FLAGS_RELEASE += -O3 27 | 28 | CLANG_FLAGS_OSX += $(CLANG_FLAGS) 29 | CLANG_FLAGS_OSX += -fobjc-arc 30 | CLANG_FLAGS_OSX += -framework Cocoa 31 | CLANG_FLAGS_OSX += -framework CoreVideo 32 | CLANG_FLAGS_OSX += -framework OpenGL 33 | CLANG_FLAGS_OSX += -lc++ 34 | CLANG_FLAGS_OSX += -o $(OSX_APP_PATH) 35 | CLANG_FLAGS_OSX += -Wl,-sectcreate,__TEXT,__info_plist,$(INFO_PLIST_PATH) 36 | CLANG_FLAGS_OSX += osx/osx.mm 37 | 38 | CLANG_FLAGS_TERMINAL += $(CLANG_FLAGS) 39 | CLANG_FLAGS_TERMINAL += -o terminal/sky 40 | CLANG_FLAGS_TERMINAL += terminal/terminal.cpp 41 | 42 | # The "ncurses" library is in a platform-dependent location 43 | ifeq ($(shell uname), Linux) 44 | CLANG_FLAGS_TERMINAL += -lncursesw # This must come last or GCC breaks 45 | else 46 | CLANG_FLAGS_TERMINAL += -lncurses 47 | endif 48 | 49 | default: debug 50 | 51 | shaders: | node_modules 52 | $(GLSLX) 53 | 54 | debug: | node_modules 55 | $(SKEW) $(SKEW_FLAGS_JS) 56 | 57 | profile: | node_modules 58 | $(SKEW) $(SKEW_FLAGS_JS) --release --js-mangle=false --js-minify=false 59 | 60 | release: | node_modules 61 | $(SKEW) $(SKEW_FLAGS_JS) --release 62 | 63 | osx-debug: | node_modules 64 | mkdir -p $(shell dirname $(OSX_APP_PATH)) 65 | $(SKEW) $(SKEW_FLAGS_OSX) 66 | echo $(INFO_PLIST_DATA) > $(INFO_PLIST_PATH) 67 | clang $(CLANG_FLAGS_OSX) 68 | rm $(INFO_PLIST_PATH) 69 | 70 | osx-release: | node_modules 71 | mkdir -p $(shell dirname $(OSX_APP_PATH)) 72 | $(SKEW) $(SKEW_FLAGS_OSX) --release --inline-functions=false 73 | echo $(INFO_PLIST_DATA) > $(INFO_PLIST_PATH) 74 | clang $(CLANG_FLAGS_OSX) $(CLANG_FLAGS_RELEASE) 75 | rm $(INFO_PLIST_PATH) 76 | 77 | terminal-debug: | node_modules 78 | $(SKEW) $(SKEW_FLAGS_TERMINAL) 79 | c++ $(CLANG_FLAGS_TERMINAL) 80 | 81 | terminal-release: | node_modules 82 | $(SKEW) $(SKEW_FLAGS_TERMINAL) 83 | c++ $(CLANG_FLAGS_TERMINAL) $(CLANG_FLAGS_RELEASE) 84 | 85 | watch-shaders: | node_modules 86 | node_modules/.bin/watch glslx 'clear && make shaders && echo done' 87 | 88 | watch-debug: | node_modules 89 | node_modules/.bin/watch src 'clear && make debug' 90 | 91 | watch-profile: | node_modules 92 | node_modules/.bin/watch src 'clear && make profile' 93 | 94 | watch-release: | node_modules 95 | node_modules/.bin/watch src 'clear && make release' 96 | 97 | node_modules: 98 | npm install 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sky Text Editor 2 | 3 | A text editor written in the Skew programming language. 4 | It uses a custom GPU-powered text editing component to render text at 60fps and implements selection, high-DPI rendering, scrollbars, keyboard shortcuts, clipboard support, syntax highlighting, tab stops, variable-width unicode characters, IME text entry, and multiple cursors. 5 | It currently targets OS X using cross-compiled C++ with OpenGL, the web using cross-compiled JavaScript with WebGL, and the terminal using cross-compiled C++ with ncurses. 6 | The web build also has a 2D canvas fallback if WebGL isn't supported so text rendering works as far back as Firefox 3.6. 7 | Because it's written in Skew, the generated JavaScript is extremely compact (under 50kb at the time of writing). 8 | This is a toy project and is not intended for real use. 9 | 10 | [Live demo](http://evanw.github.io/sky/) 11 | -------------------------------------------------------------------------------- /glslx/shaders.glslx: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | uniform sampler2D texture; 4 | uniform vec2 dropShadowOptions; 5 | uniform vec2 scale2; 6 | uniform vec4 dropShadowBox; 7 | uniform bool applyGammaHack; 8 | 9 | attribute vec2 position2; 10 | attribute vec4 color4; 11 | attribute vec4 coord4; 12 | attribute vec4 position4; 13 | 14 | varying vec2 _position2; 15 | varying vec4 _color4; 16 | varying vec4 _coord4; 17 | varying vec4 _position4; 18 | 19 | //////////////////////////////////////////////////////////////////////////////// 20 | 21 | export void solidBatchVertex() { 22 | _coord4 = position4; 23 | _color4 = color4; 24 | gl_Position = vec4(position4.xy * scale2 - sign(scale2), 0, 1); 25 | } 26 | 27 | export void solidBatchFragment() { 28 | gl_FragColor = _color4 * min(1.0, min(_coord4.z, _coord4.w)); 29 | } 30 | 31 | //////////////////////////////////////////////////////////////////////////////// 32 | 33 | export void glyphBatchVertex() { 34 | _coord4 = vec4(position4.zw, coord4.x * 3.0, coord4.y); 35 | _color4 = color4; 36 | gl_Position = vec4(position4.xy * scale2 - sign(scale2), 0, 1); 37 | } 38 | 39 | export void glyphBatchFragment() { 40 | vec4 mask = texture2D(texture, _coord4.xy); 41 | float bg = _coord4.w; 42 | float fg = dot(_color4.rgb, vec3(0.299, 0.587, 0.114)); 43 | float alpha = 44 | _coord4.z < 1.0 ? mask.x : 45 | _coord4.z < 2.0 ? mask.y : 46 | mask.z; 47 | 48 | // Attempt to correct for some of the gamma-related "bolding" that the browser adds 49 | // to the mask values since we know the real background and foreground colors 50 | if (applyGammaHack) { 51 | alpha += ((sqrt(mix(bg * bg, fg * fg, sqrt(alpha))) - bg) / (fg - bg) - alpha) * 0.5; 52 | } 53 | 54 | gl_FragColor = _color4 * alpha; 55 | } 56 | 57 | //////////////////////////////////////////////////////////////////////////////// 58 | 59 | // This approximates the error function, needed for the gaussian integral 60 | vec4 erf(vec4 x) { 61 | vec4 s = sign(x), a = abs(x); 62 | x = a * (a * (a * a * 0.078108 + 0.230389) + 0.278393) + 1.0; 63 | x *= x; 64 | return s - s / (x * x); 65 | } 66 | 67 | // Return the mask for the shadow of a box from lower to upper 68 | float boxShadow(vec2 lower, vec2 upper, vec2 point, float sigma) { 69 | vec4 query = vec4(point - lower, point - upper); 70 | vec4 integral = 0.5 + 0.5 * erf(query * (sqrt(0.5) / sigma)); 71 | return (integral.z - integral.x) * (integral.w - integral.y); 72 | } 73 | 74 | // A per-pixel "random" number between 0 and 1 75 | float random() { 76 | return fract(sin(dot(vec2(12.9898, 78.233), gl_FragCoord.xy)) * 43758.5453); 77 | } 78 | 79 | export void dropShadowVertex() { 80 | _position2 = position2; 81 | gl_Position = vec4(position2.xy * scale2 - sign(scale2), 0, 1); 82 | } 83 | 84 | export void dropShadowFragment() { 85 | float alpha = dropShadowOptions.x * boxShadow(dropShadowBox.xy, dropShadowBox.zw, _position2.xy, dropShadowOptions.y); 86 | 87 | // Dither the alpha to break up color bands 88 | alpha += (random() - 0.5) / 128.0; 89 | 90 | gl_FragColor = vec4(0, 0, 0, alpha); 91 | } 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "fast-watch": "1.0.0", 4 | "glslx": "0.1.18", 5 | "skew": "0.7.40" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/browser/canvas.sk: -------------------------------------------------------------------------------- 1 | namespace Browser { 2 | class CanvasElement :: UI.PixelRenderer { 3 | var _width = 0 4 | var _height = 0 5 | var _pixelScale = 0.0 6 | var _isRendering = false 7 | var _element = document.createElement("canvas") 8 | var _mousemove fn(dynamic) = null 9 | var _mouseup fn(dynamic) = null 10 | var _window Window = null 11 | var _translator UI.SemanticToPixelTranslator = null 12 | var _fontContext = document.createElement("canvas").getContext("2d") 13 | var _fontInstances IntMap = {} 14 | 15 | def new(window Window) { 16 | _window = window 17 | _translator = UI.SemanticToPixelTranslator.new(self) 18 | 19 | var style = _element.style 20 | style.width = "100%" 21 | style.height = "100%" 22 | } 23 | 24 | def _afterResize 25 | def setViewport(x double, y double, width double, height double) 26 | def setDefaultBackgroundColor(color Graphics.RGBA) 27 | def fillRect(x double, y double, width double, height double, color Graphics.RGBA) 28 | def fillRoundedRect(x double, y double, width double, height double, color Graphics.RGBA, radius double) 29 | def strokePolyline(coordinates List, color Graphics.RGBA, thickness double) 30 | def renderText(x double, y double, text string, font UI.Font, color Graphics.RGBA) 31 | def renderRectShadow( 32 | boxX double, boxY double, boxWidth double, boxHeight double, 33 | clipX double, clipY double, clipWidth double, clipHeight double, 34 | shadowAlpha double, blurSigma double) 35 | 36 | def element dynamic { 37 | return _element 38 | } 39 | 40 | def translator UI.SemanticToPixelTranslator { 41 | return _translator 42 | } 43 | 44 | def width int { 45 | return _width 46 | } 47 | 48 | def height int { 49 | return _height 50 | } 51 | 52 | def pixelScale double { 53 | return _pixelScale 54 | } 55 | 56 | def fontInstance(font UI.Font) UI.FontInstance { 57 | return _fontInstances.get(font, null) 58 | } 59 | 60 | def advanceWidth(font UI.Font, codePoint int) double { 61 | return _fontContext 62 | } 63 | 64 | def setFont(font UI.Font, names List, size double, height double, flags UI.FontFlags) { 65 | _fontInstances[font] = FontInstance.new(font, names, size, height, flags, _fontInstanceScale) 66 | } 67 | 68 | def beginFrame { 69 | _isRendering = true 70 | } 71 | 72 | def endFrame { 73 | _isRendering = false 74 | } 75 | 76 | def resize(width int, height int, pixelScale double) { 77 | if _width != width || _height != height || _pixelScale != pixelScale { 78 | _width = width 79 | _height = height 80 | _pixelScale = pixelScale 81 | _element.width = Math.round(width * pixelScale) 82 | _element.height = Math.round(height * pixelScale) 83 | _fontInstances.each((key, instance) => instance.changePixelScale(_fontInstanceScale)) 84 | _afterResize 85 | } 86 | } 87 | 88 | def _fontInstanceScale double { 89 | return 1 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/browser/canvas2d.sk: -------------------------------------------------------------------------------- 1 | namespace Browser { 2 | class CanvasElement2D : CanvasElement { 3 | var _context dynamic 4 | var _cachedStyles IntMap = {} 5 | var _hasViewport = false 6 | var _previousFont UI.Font = .CODE_FONT 7 | var _isPreviousFontValid = false 8 | 9 | def new(window Window) { 10 | super(window) 11 | _context = _element.getContext("2d") 12 | 13 | Log.info("initialized 2D canvas") 14 | } 15 | 16 | over setDefaultBackgroundColor(color Graphics.RGBA) { 17 | _element.style.background = _colorToStyle(color) 18 | } 19 | 20 | over setViewport(x double, y double, width double, height double) { 21 | if _hasViewport { 22 | _context.restore() 23 | } 24 | 25 | _context.save() 26 | _context.translate(x, y) 27 | _context.beginPath() 28 | _context.rect(0, 0, width, height) 29 | _context.clip() 30 | _hasViewport = true 31 | } 32 | 33 | over _afterResize { 34 | _context.scale(_pixelScale, _pixelScale) 35 | } 36 | 37 | over beginFrame { 38 | super 39 | 40 | _isPreviousFontValid = false 41 | _context.clearRect(0, 0, _width, _height) 42 | } 43 | 44 | over endFrame { 45 | super 46 | 47 | if _hasViewport { 48 | _context.restore() 49 | _hasViewport = false 50 | } 51 | } 52 | 53 | over fillRect(x double, y double, width double, height double, color Graphics.RGBA) { 54 | assert(_isRendering) 55 | 56 | if x >= _width || y >= _height || x + width <= 0 || y + height <= 0 { 57 | return 58 | } 59 | 60 | _context.fillStyle = _colorToStyle(color) 61 | _context.fillRect(x, y, width, height) 62 | } 63 | 64 | over fillRoundedRect(x double, y double, width double, height double, color Graphics.RGBA, radius double) { 65 | assert(_isRendering) 66 | 67 | if x >= _width || y >= _height || x + width <= 0 || y + height <= 0 { 68 | return 69 | } 70 | 71 | radius = Math.min(radius, width / 2, height / 2) 72 | 73 | const BEZIER_CIRCLE_CONSTANT = (7 - 4 * Math.SQRT_2) / 3 74 | var context = _context 75 | var r = radius * BEZIER_CIRCLE_CONSTANT 76 | var xw = x + width 77 | var yh = y + height 78 | 79 | # Render an approximate rounded rectangle 80 | context.fillStyle = _colorToStyle(color) 81 | context.beginPath() 82 | _lineAndCurveTo(context, x, y + radius, x, y + r, x + r, y, x + radius, y) 83 | _lineAndCurveTo(context, xw - radius, y, xw - r, y, xw, y + r, xw, y + radius) 84 | _lineAndCurveTo(context, xw, yh - radius, xw, yh - r, xw - r, yh, xw - radius, yh) 85 | _lineAndCurveTo(context, x + radius, yh, x + r, yh, x, yh - r, x, yh - radius) 86 | context.fill() 87 | } 88 | 89 | over strokePolyline(coordinates List, color Graphics.RGBA, thickness double) { 90 | assert(_isRendering) 91 | 92 | assert(coordinates.count % 2 == 0) 93 | 94 | var context = _context 95 | context.strokeStyle = _colorToStyle(color) 96 | context.lineWidth = thickness 97 | context.beginPath() 98 | 99 | for i = 0; i < coordinates.count; i += 2 { 100 | context.lineTo(coordinates[i], coordinates[i + 1]) 101 | } 102 | 103 | context.stroke() 104 | } 105 | 106 | over renderRectShadow( 107 | rectX double, rectY double, rectWidth double, rectHeight double, 108 | clipX double, clipY double, clipWidth double, clipHeight double, 109 | shadowAlpha double, blurSigma double) { 110 | 111 | assert(_isRendering) 112 | 113 | if clipX >= _width || clipY >= _height || clipX + clipWidth <= 0 || clipY + clipHeight <= 0 { 114 | return 115 | } 116 | 117 | var context = _context 118 | var offset = 1000 119 | context.save() 120 | context.rect(clipX, clipY, clipWidth, clipHeight) 121 | context.clip() 122 | context.shadowColor = "rgba(0,0,0,\(shadowAlpha))" 123 | context.shadowOffsetX = offset * _pixelScale 124 | context.shadowOffsetY = offset * _pixelScale 125 | context.shadowBlur = blurSigma * 3 * _pixelScale 126 | context.fillStyle = "#000" 127 | context.fillRect(rectX - offset, rectY - offset, rectWidth, rectHeight) 128 | context.restore() 129 | } 130 | 131 | over renderText(x double, y double, text string, font UI.Font, color Graphics.RGBA) { 132 | assert(_isRendering) 133 | 134 | var fontInstance = _fontInstances.get(font, null) 135 | if fontInstance == null || x >= _width || y >= _height || y + fontInstance.size <= 0 { 136 | return 137 | } 138 | 139 | # Assigning to the font is really expensive in Chrome even if it's the same value 140 | if !_isPreviousFontValid || font != _previousFont { 141 | _previousFont = font 142 | _isPreviousFontValid = true 143 | _context.font = fontInstance.canvasText 144 | } 145 | 146 | _context.fillStyle = _colorToStyle(color) 147 | _context.fillText(text, x, y + fontInstance.size) 148 | } 149 | 150 | def _colorToStyle(color Graphics.RGBA) string { 151 | var style = _cachedStyles.get(color as int, null) 152 | if style == null { 153 | style = "rgba(\(color.red),\(color.green),\(color.blue),\(color.alpha / 255.0))" 154 | _cachedStyles[color as int] = style 155 | } 156 | return style 157 | } 158 | } 159 | 160 | namespace CanvasElement2D { 161 | def _lineAndCurveTo(context dynamic, ax double, ay double, bx double, by double, cx double, cy double, dx double, dy double) { 162 | context.lineTo(ax, ay) 163 | context.bezierCurveTo(bx, by, cx, cy, dx, dy) 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/browser/canvaswebgl.sk: -------------------------------------------------------------------------------- 1 | namespace Browser { 2 | class CanvasElementWebGL : CanvasElement { 3 | var _context Context = null 4 | var _solidBatch Graphics.SolidBatch = null 5 | var _glyphBatch Graphics.GlyphBatch = null 6 | var _dropShadow Graphics.DropShadow = null 7 | var _clearColor Graphics.RGBA = .TRANSPARENT 8 | 9 | def new(window Window) { 10 | super(window) 11 | 12 | var gl WebGLRenderingContext = null 13 | var options dynamic = { 14 | "alpha": false, 15 | "antialias": false, 16 | "depth": false, 17 | "failIfMajorPerformanceCaveat": true, 18 | "preserveDrawingBuffer": true, 19 | "stencil": false, 20 | } 21 | 22 | # Attempt to use the official WebGL string 23 | try { 24 | gl = _element.getContext("webgl", options) 25 | } 26 | 27 | # Fall back to the older unofficial WebGL string 28 | gl ?= _element.getContext("experimental-webgl", options) 29 | 30 | _context = Context.new(gl) 31 | _solidBatch = Graphics.SolidBatch.new(_context) 32 | _glyphBatch = Graphics.GlyphBatch.new(window.platform, _context) 33 | _dropShadow = Graphics.DropShadow.new(_context) 34 | 35 | Log.info("initialized WebGL canvas") 36 | } 37 | 38 | over setDefaultBackgroundColor(color Graphics.RGBA) { 39 | _glyphBatch.setBackgroundColor(color) 40 | _clearColor = color 41 | } 42 | 43 | over setViewport(x double, y double, width double, height double) { 44 | _solidBatch.flush 45 | _glyphBatch.flush 46 | 47 | _context.setViewport( 48 | Math.round(x * _pixelScale) as int, 49 | Math.round(y * _pixelScale) as int, 50 | Math.round(width * _pixelScale) as int, 51 | Math.round(height * _pixelScale) as int) 52 | 53 | _solidBatch.resize(width, height, _pixelScale) 54 | _glyphBatch.resize(width, height, _pixelScale) 55 | _dropShadow.resize(width, height) 56 | } 57 | 58 | over _afterResize { 59 | _context.resize(_element.width, _element.height) 60 | setViewport(0, 0, _width, _height) 61 | } 62 | 63 | over beginFrame { 64 | super 65 | _context.clear(_clearColor) 66 | } 67 | 68 | over endFrame { 69 | super 70 | _solidBatch.flush 71 | _glyphBatch.flush 72 | } 73 | 74 | over fillRect(x double, y double, width double, height double, color Graphics.RGBA) { 75 | assert(_isRendering) 76 | 77 | if x >= _width || y >= _height || x + width <= 0 || y + height <= 0 { 78 | return 79 | } 80 | 81 | _glyphBatch.flush 82 | _solidBatch.fillRect(x, y, width, height, color.premultiplied) 83 | } 84 | 85 | over fillRoundedRect(x double, y double, width double, height double, color Graphics.RGBA, radius double) { 86 | assert(_isRendering) 87 | 88 | if x >= _width || y >= _height || x + width <= 0 || y + height <= 0 { 89 | return 90 | } 91 | 92 | _glyphBatch.flush 93 | _solidBatch.fillRoundedRect(x, y, width, height, color.premultiplied, radius) 94 | } 95 | 96 | over strokePolyline(coordinates List, color Graphics.RGBA, thickness double) { 97 | assert(_isRendering) 98 | 99 | assert(coordinates.count % 2 == 0) 100 | _glyphBatch.flush 101 | _solidBatch.strokeNonOverlappingPolyline(coordinates, color.premultiplied, thickness, .OPEN) 102 | } 103 | 104 | over renderRectShadow( 105 | rectX double, rectY double, rectWidth double, rectHeight double, 106 | clipX double, clipY double, clipWidth double, clipHeight double, 107 | shadowAlpha double, blurSigma double) { 108 | 109 | assert(_isRendering) 110 | 111 | if clipX >= _width || clipY >= _height || clipX + clipWidth <= 0 || clipY + clipHeight <= 0 { 112 | return 113 | } 114 | 115 | _solidBatch.flush 116 | _glyphBatch.flush 117 | _dropShadow.render(rectX, rectY, rectWidth, rectHeight, clipX, clipY, clipWidth, clipHeight, shadowAlpha, blurSigma) 118 | } 119 | 120 | over renderText(x double, y double, text string, font UI.Font, color Graphics.RGBA) { 121 | assert(_isRendering) 122 | 123 | var fontInstance = _fontInstances.get(font, null) 124 | if fontInstance == null || x >= _width || y >= _height || y + fontInstance.size <= 0 { 125 | return 126 | } 127 | 128 | var iterator = Unicode.StringIterator.INSTANCE.reset(text, 0) 129 | 130 | _solidBatch.flush 131 | color = color.premultiplied 132 | 133 | for codePoint = iterator.nextCodePoint; codePoint != -1; codePoint = iterator.nextCodePoint { 134 | x += _glyphBatch.appendGlyph(fontInstance, codePoint, x, y, color) 135 | } 136 | } 137 | 138 | over _fontInstanceScale double { 139 | return _pixelScale 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/browser/fontinstance.sk: -------------------------------------------------------------------------------- 1 | namespace Browser { 2 | class FontInstance :: UI.FontInstance { 3 | const _font UI.Font 4 | const _names List 5 | const _size double 6 | const _lineHeight double 7 | const _flags UI.FontFlags 8 | const _advanceWidthCache IntMap = {} 9 | const _context = document.createElement("canvas").getContext("2d") 10 | var _maskData Int32Array = null 11 | var _pixelScale = 0.0 12 | var _canvasText = "" 13 | var _canvasSize = 0 14 | 15 | def new(font UI.Font, names List, size double, lineHeight double, flags UI.FontFlags, pixelScale double) { 16 | _font = font 17 | _names = names 18 | _size = size 19 | _lineHeight = lineHeight 20 | _flags = flags 21 | changePixelScale(pixelScale) 22 | } 23 | 24 | def font UI.Font { 25 | return _font 26 | } 27 | 28 | def size double { 29 | return _size 30 | } 31 | 32 | def lineHeight double { 33 | return _lineHeight 34 | } 35 | 36 | def flags UI.FontFlags { 37 | return _flags 38 | } 39 | 40 | def canvasText string { 41 | return _canvasText 42 | } 43 | 44 | def advanceWidth(codePoint int) double { 45 | var value = _advanceWidthCache.get(codePoint, -1) 46 | 47 | # Measure 100 successive characters in an attempt to get a better 48 | # measurement since some browsers never return a fractional value 49 | if value == -1 { 50 | value = _context.measureText(string.fromCodePoint(codePoint).repeat(100)).width / (100 * _pixelScale) 51 | _advanceWidthCache[codePoint] = value 52 | } 53 | 54 | return value 55 | } 56 | 57 | def changePixelScale(pixelScale double) { 58 | if pixelScale != _pixelScale { 59 | var fontSize = size * pixelScale 60 | _pixelScale = pixelScale 61 | _canvasText = (.ITALIC in _flags ? "italic " : "") + (.BOLD in _flags ? "bold " : "") + "\(fontSize)px " + (", ".join(_names)) 62 | 63 | # Assume glyphs won't ever grow extend out more than twice the font size. 64 | # Because the canvas text API is so terrible, we have no way of knowing 65 | # what the actual glyph size is, so this is the best we can do. 66 | var canvasSize = Math.ceil(fontSize * 2) as int 67 | _context.canvas.width = canvasSize 68 | _context.canvas.height = canvasSize 69 | 70 | # This must be set after resizing the canvas or it will be reset 71 | _context.font = _canvasText 72 | _maskData = Int32Array.new(canvasSize * canvasSize) 73 | _canvasSize = canvasSize 74 | } 75 | } 76 | 77 | def renderGlyph(codePoint int) Graphics.Glyph { 78 | # Round the origin coordinates because some browsers do hinting 79 | var size = _canvasSize 80 | var originX = size / 4 81 | var originY = size / 4 82 | var maskData = _maskData 83 | 84 | # Render the glyph three times at different offsets 85 | for i in 0..3 { 86 | _context.clearRect(0, 0, size, size) 87 | _context.fillText(string.fromCodePoint(codePoint), originX + i / 3.0, originY + _size * _pixelScale) 88 | 89 | var data Uint8ClampedArray = _context.getImageData(0, 0, size, size).data 90 | var shift = i * 8 91 | 92 | assert(data.length == maskData.length * 4) 93 | 94 | for j = 0, k = 3; j < maskData.length; j++, k += 4 { 95 | maskData[j] = shift != 0 ? maskData[j] | data[k] << shift : data[k] 96 | } 97 | } 98 | 99 | # Trim the image in preparation for using it in an atlas texture 100 | var minX = 0 101 | var minY = 0 102 | var maxX = size 103 | var maxY = size 104 | 105 | # Trim the left 106 | for found = false; minX < maxX; minX++ { 107 | for y = minY, i = minX + y * size; !found && y < maxY; y++, i += size { 108 | found = maskData[i] > 0 109 | } 110 | if found { 111 | break 112 | } 113 | } 114 | 115 | # Trim the right 116 | for found = false; minX < maxX; maxX-- { 117 | for y = minY, i = maxX - 1 + y * size; !found && y < maxY; y++, i += size { 118 | found = maskData[i] > 0 119 | } 120 | if found { 121 | break 122 | } 123 | } 124 | 125 | # Trim the top 126 | for found = false; minY < maxY; minY++ { 127 | for x = minX, i = x + minY * size; !found && x < maxX; x++, i++ { 128 | found = maskData[i] > 0 129 | } 130 | if found { 131 | break 132 | } 133 | } 134 | 135 | # Trim the bottom 136 | for found = false; minY < maxY; maxY-- { 137 | for x = minX, i = x + (maxY - 1) * size; !found && x < maxX; x++, i++ { 138 | found = maskData[i] > 0 139 | } 140 | if found { 141 | break 142 | } 143 | } 144 | 145 | # Compact the mask into a linear array of memory 146 | var width = maxX - minX 147 | var height = maxY - minY 148 | var mask = Graphics.Mask.new(width, height) 149 | var output = mask.pixels 150 | for y = 0, to = 0; y < height; y++ { 151 | for x = 0, from = minX + (minY + y) * size; x < width; x++, from++, to += 4 { 152 | output.setByte(to, maskData[from]) 153 | output.setByte(to + 1, maskData[from] >> 8) 154 | output.setByte(to + 2, maskData[from] >> 16) 155 | } 156 | } 157 | 158 | return Graphics.Glyph.new(codePoint, mask, originX - minX, originY - minY, 1 / _pixelScale, advanceWidth(codePoint)) 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/browser/support.sk: -------------------------------------------------------------------------------- 1 | namespace Browser { 2 | @neverinline 3 | def on(element dynamic, event string, function fn(dynamic)) { 4 | element.addEventListener(event, function, false) 5 | } 6 | 7 | @neverinline 8 | def off(element dynamic, event string, function fn(dynamic)) { 9 | element.removeEventListener(event, function, false) 10 | } 11 | 12 | var document = dynamic.document 13 | var window = dynamic.window 14 | } 15 | -------------------------------------------------------------------------------- /src/browser/typedarray.sk: -------------------------------------------------------------------------------- 1 | @import 2 | class ArrayBuffer { 3 | const byteLength int 4 | 5 | def new(length int) 6 | def slice(begin int) ArrayBuffer 7 | def slice(begin int, end int) ArrayBuffer 8 | } 9 | 10 | namespace ArrayBuffer { 11 | def isView(value dynamic) bool 12 | } 13 | 14 | @import 15 | class ArrayBufferView { 16 | const buffer ArrayBuffer 17 | const byteOffset int 18 | const byteLength int 19 | } 20 | 21 | @import 22 | class Int8Array : ArrayBufferView { 23 | const length int 24 | 25 | @prefer 26 | def new(length int) 27 | def new(array Int8Array) 28 | def new(array List) 29 | def new(buffer ArrayBuffer) 30 | def new(buffer ArrayBuffer, byteOffset int) 31 | def new(buffer ArrayBuffer, byteOffset int, length int) 32 | 33 | def [](index int) int 34 | def []=(index int, value int) 35 | def set(array Int8Array) 36 | def set(array Int8Array, offset int) 37 | def set(array List) 38 | def set(array List, offset int) 39 | def subarray(begin int) Int8Array 40 | def subarray(begin int, end int) Int8Array 41 | } 42 | 43 | @import 44 | class Uint8Array : ArrayBufferView { 45 | const length int 46 | 47 | @prefer 48 | def new(length int) 49 | def new(array Uint8Array) 50 | def new(array List) 51 | def new(buffer ArrayBuffer) 52 | def new(buffer ArrayBuffer, byteOffset int) 53 | def new(buffer ArrayBuffer, byteOffset int, length int) 54 | 55 | def [](index int) int 56 | def []=(index int, value int) 57 | def set(array Uint8Array) 58 | def set(array Uint8Array, offset int) 59 | def set(array List) 60 | def set(array List, offset int) 61 | def subarray(begin int) Uint8Array 62 | def subarray(begin int, end int) Uint8Array 63 | } 64 | 65 | @import 66 | class Uint8ClampedArray : ArrayBufferView { 67 | const length int 68 | 69 | @prefer 70 | def new(length int) 71 | def new(array Uint8ClampedArray) 72 | def new(array List) 73 | def new(buffer ArrayBuffer) 74 | def new(buffer ArrayBuffer, byteOffset int) 75 | def new(buffer ArrayBuffer, byteOffset int, length int) 76 | 77 | def [](index int) int 78 | def []=(index int, value int) 79 | def set(array Uint8ClampedArray) 80 | def set(array Uint8ClampedArray, offset int) 81 | def set(array List) 82 | def set(array List, offset int) 83 | def subarray(begin int) Uint8ClampedArray 84 | def subarray(begin int, end int) Uint8ClampedArray 85 | } 86 | 87 | @import 88 | class Int16Array : ArrayBufferView { 89 | const length int 90 | 91 | @prefer 92 | def new(length int) 93 | def new(array Int16Array) 94 | def new(array List) 95 | def new(buffer ArrayBuffer) 96 | def new(buffer ArrayBuffer, byteOffset int) 97 | def new(buffer ArrayBuffer, byteOffset int, length int) 98 | 99 | def [](index int) int 100 | def []=(index int, value int) 101 | def set(array Int16Array) 102 | def set(array Int16Array, offset int) 103 | def set(array List) 104 | def set(array List, offset int) 105 | def subarray(begin int) Int16Array 106 | def subarray(begin int, end int) Int16Array 107 | } 108 | 109 | @import 110 | class Uint16Array : ArrayBufferView { 111 | const length int 112 | 113 | @prefer 114 | def new(length int) 115 | def new(array Uint16Array) 116 | def new(array List) 117 | def new(buffer ArrayBuffer) 118 | def new(buffer ArrayBuffer, byteOffset int) 119 | def new(buffer ArrayBuffer, byteOffset int, length int) 120 | 121 | def [](index int) int 122 | def []=(index int, value int) 123 | def set(array Uint16Array) 124 | def set(array Uint16Array, offset int) 125 | def set(array List) 126 | def set(array List, offset int) 127 | def subarray(begin int) Uint16Array 128 | def subarray(begin int, end int) Uint16Array 129 | } 130 | 131 | @import 132 | class Int32Array : ArrayBufferView { 133 | const length int 134 | 135 | @prefer 136 | def new(length int) 137 | def new(array Int32Array) 138 | def new(array List) 139 | def new(buffer ArrayBuffer) 140 | def new(buffer ArrayBuffer, byteOffset int) 141 | def new(buffer ArrayBuffer, byteOffset int, length int) 142 | 143 | def [](index int) int 144 | def []=(index int, value int) 145 | def set(array Int32Array) 146 | def set(array Int32Array, offset int) 147 | def set(array List) 148 | def set(array List, offset int) 149 | def subarray(begin int) Int32Array 150 | def subarray(begin int, end int) Int32Array 151 | } 152 | 153 | @import 154 | class Uint32Array : ArrayBufferView { 155 | const length int 156 | 157 | @prefer 158 | def new(length int) 159 | def new(array Uint32Array) 160 | def new(array List) 161 | def new(buffer ArrayBuffer) 162 | def new(buffer ArrayBuffer, byteOffset int) 163 | def new(buffer ArrayBuffer, byteOffset int, length int) 164 | 165 | def [](index int) int 166 | def []=(index int, value int) 167 | def set(array Uint32Array) 168 | def set(array Uint32Array, offset int) 169 | def set(array List) 170 | def set(array List, offset int) 171 | def subarray(begin int) Uint32Array 172 | def subarray(begin int, end int) Uint32Array 173 | } 174 | 175 | @import 176 | class Float32Array : ArrayBufferView { 177 | const length int 178 | 179 | @prefer 180 | def new(length int) 181 | def new(array Float32Array) 182 | def new(array List) 183 | def new(buffer ArrayBuffer) 184 | def new(buffer ArrayBuffer, byteOffset int) 185 | def new(buffer ArrayBuffer, byteOffset int, length int) 186 | 187 | def [](index int) double 188 | def []=(index int, value double) 189 | def set(array Float32Array) 190 | def set(array Float32Array, offset int) 191 | def set(array List) 192 | def set(array List, offset int) 193 | def subarray(begin int) Float32Array 194 | def subarray(begin int, end int) Float32Array 195 | } 196 | 197 | @import 198 | class Float64Array : ArrayBufferView { 199 | const length int 200 | 201 | @prefer 202 | def new(length int) 203 | def new(array Float64Array) 204 | def new(array List) 205 | def new(buffer ArrayBuffer) 206 | def new(buffer ArrayBuffer, byteOffset int) 207 | def new(buffer ArrayBuffer, byteOffset int, length int) 208 | 209 | def [](index int) double 210 | def []=(index int, value double) 211 | def set(array Float64Array) 212 | def set(array Float64Array, offset int) 213 | def set(array List) 214 | def set(array List, offset int) 215 | def subarray(begin int) Float64Array 216 | def subarray(begin int, end int) Float64Array 217 | } 218 | 219 | @import 220 | class DataView : ArrayBufferView { 221 | def new(buffer ArrayBuffer) 222 | def new(buffer ArrayBuffer, byteOffset int) 223 | def new(buffer ArrayBuffer, byteOffset int, byteLength int) 224 | 225 | def getInt8(byteOffset int) int 226 | def getUint8(byteOffset int) int 227 | def getInt16(byteOffset int) int 228 | def getUint16(byteOffset int) int 229 | def getInt16(byteOffset int, littleEndian bool) int 230 | def getUint16(byteOffset int, littleEndian bool) int 231 | def getInt32(byteOffset int) int 232 | def getUint32(byteOffset int) int 233 | def getInt32(byteOffset int, littleEndian bool) int 234 | def getUint32(byteOffset int, littleEndian bool) int 235 | def getFloat32(byteOffset int) double 236 | def getFloat64(byteOffset int) double 237 | def getFloat32(byteOffset int, littleEndian bool) double 238 | def getFloat64(byteOffset int, littleEndian bool) double 239 | 240 | def setInt8(byteOffset int, value int) 241 | def setUint8(byteOffset int, value int) 242 | def setInt16(byteOffset int, value int) 243 | def setUint16(byteOffset int, value int) 244 | def setInt16(byteOffset int, value int, littleEndian bool) 245 | def setUint16(byteOffset int, value int, littleEndian bool) 246 | def setInt32(byteOffset int, value int) 247 | def setUint32(byteOffset int, value int) 248 | def setInt32(byteOffset int, value int, littleEndian bool) 249 | def setUint32(byteOffset int, value int, littleEndian bool) 250 | def setFloat32(byteOffset int, value double) 251 | def setFloat64(byteOffset int, value double) 252 | def setFloat32(byteOffset int, value double, littleEndian bool) 253 | def setFloat64(byteOffset int, value double, littleEndian bool) 254 | } 255 | 256 | namespace DataView { 257 | def new(array ArrayBufferView) DataView { 258 | return new(array.buffer, array.byteOffset, array.byteLength) 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/browser/window.sk: -------------------------------------------------------------------------------- 1 | namespace Browser { 2 | enum DeltaMode { 3 | PIXEL 4 | LINE 5 | PAGE 6 | } 7 | 8 | const _whichToKeyCode IntMap = { 9 | 8: .BACKSPACE, 10 | 9: .TAB, 11 | 13: .ENTER, 12 | 27: .ESCAPE, 13 | 33: .PAGE_UP, 14 | 34: .PAGE_DOWN, 15 | 35: .END, 16 | 36: .HOME, 17 | 37: .ARROW_LEFT, 18 | 38: .ARROW_UP, 19 | 39: .ARROW_RIGHT, 20 | 40: .ARROW_DOWN, 21 | 44: .COMMA, 22 | 46: .DELETE, 23 | 59: .SEMICOLON, 24 | 186: .SEMICOLON, 25 | 188: .COMMA, 26 | 190: .PERIOD, 27 | } 28 | 29 | class Window : UI.Window :: UI.Platform { 30 | const _operatingSystem UI.OperatingSystem 31 | const _userAgent UI.UserAgent 32 | const _canvas CanvasElement 33 | const _element = document.createElement("div") 34 | const _input = document.createElement("input") 35 | const _shortcuts Editor.ShortcutMap 36 | var _windowHasFocus = false 37 | var _draggingView UI.View = null 38 | var _mousemove fn(dynamic) = null 39 | var _mouseup fn(dynamic) = null 40 | var _window UI.Window = null 41 | 42 | # Clipboard support 43 | var _cutAndCopyAreProbablyBroken = false 44 | var _previousClipboard string = null 45 | var _fakeClipboard = "" 46 | 47 | def new { 48 | super 49 | 50 | var platform string = dynamic.navigator.platform 51 | var userAgent string = dynamic.navigator.userAgent 52 | var vendor string = dynamic.navigator.vendor 53 | 54 | _operatingSystem = 55 | # OS X encodes the architecture into the platform 56 | platform == "MacIntel" || platform == "MacPPC" ? .OSX : 57 | 58 | # MSDN sources say Win64 is used 59 | platform == "Win32" || platform == "Win64" ? .WINDOWS : 60 | 61 | # Assume the user is using Mobile Safari or Chrome and not some random 62 | # browser with a strange platform (Opera apparently messes with this) 63 | platform == "iPhone" || platform == "iPad" ? .IOS : 64 | 65 | # Apparently most Android devices have a platform of "Linux" instead 66 | # of "Android", so check the user agent instead. Also make sure to test 67 | # for Android before Linux for this reason. 68 | "Android" in userAgent ? .ANDROID : 69 | "Linux" in platform ? .LINUX : 70 | 71 | # The platform string has no specification and can be literally anything. 72 | # Other examples: "BlackBerry", "Nintendo 3DS", "PlayStation 4", etc. 73 | .UNKNOWN 74 | 75 | _userAgent = 76 | "Trident" in userAgent ? .IE : 77 | "Edge" in userAgent ? .EDGE : 78 | "Chrome" in userAgent ? .CHROME : 79 | "Firefox" in userAgent ? .FIREFOX : 80 | "Apple" in vendor ? .SAFARI : 81 | .UNKNOWN 82 | 83 | # Bug: Safari/IE fail to fire cut/copy events if the input element is empty (http://caniuse.com/clipboard) 84 | _cutAndCopyAreProbablyBroken = _userAgent == .IE || _userAgent == .EDGE || _userAgent == .SAFARI 85 | 86 | _shortcuts = Editor.ShortcutMap.new(self) 87 | _canvas = _createCanvas 88 | _createInput 89 | _startAnimationLoop 90 | _prepareElement 91 | _window = self 92 | } 93 | 94 | over platform UI.Platform { 95 | return self 96 | } 97 | 98 | over setTitle(title string) { 99 | document.title = title 100 | } 101 | 102 | over setTheme(theme UI.Theme) { 103 | _canvas.translator.setTheme(theme) 104 | } 105 | 106 | over setCursor(cursor UI.Cursor) { 107 | _element.style.cursor = 108 | cursor == .TEXT ? "text" : 109 | "default" 110 | } 111 | 112 | over renderer UI.SemanticRenderer { 113 | return _canvas.translator 114 | } 115 | 116 | over render { 117 | var translator = _canvas.translator 118 | _canvas.beginFrame 119 | translator.renderView(root) 120 | _canvas.endFrame 121 | 122 | # Move the using a transform to avoid a layout. The must 123 | # be moved near the cursor so IME UI makes sense (completion list, etc.). 124 | _input.style.transform = "translate(\(translator.lastCaretX)px, \(translator.lastCaretY)px)" 125 | } 126 | 127 | over setFont(font UI.Font, names List, size double, height double, flags UI.FontFlags) { 128 | if font == .CODE_FONT { 129 | _input.style.height = "\(height)px" 130 | } 131 | _canvas.setFont(font, names, size, height, flags) 132 | } 133 | 134 | def element dynamic { 135 | return _element 136 | } 137 | 138 | def stretchToFitBody { 139 | var style = element.style 140 | style.position = "fixed" 141 | style.left = "0" 142 | style.top = "0" 143 | style.right = "0" 144 | style.bottom = "0" 145 | style.overflow = "hidden" 146 | document.body.appendChild(element) 147 | 148 | var onresize = (event dynamic) => { 149 | resize(window.innerWidth, window.innerHeight) 150 | } 151 | 152 | var onfocus = (event dynamic) => { 153 | _windowHasFocus = true 154 | _setHasFocus(true) 155 | } 156 | 157 | on(window, "resize", onresize) 158 | on(window, "focus", onfocus) 159 | 160 | on(window, "blur", e => { 161 | _windowHasFocus = false 162 | _updateIsActive 163 | }) 164 | 165 | on(document, "visibilitychange", e => { 166 | _updateIsActive 167 | }) 168 | 169 | onresize(null) 170 | onfocus(null) 171 | } 172 | 173 | def resize(width int, height int) { 174 | var pixelScale double = window.devicePixelRatio ? window.devicePixelRatio : 1 175 | var style = element.style 176 | 177 | # Safari broke devicePixelRatio 178 | if _userAgent == .SAFARI { 179 | pixelScale *= (document.width / window.innerWidth + document.height / window.innerHeight) / 2 180 | } 181 | 182 | style.width = "\(width)px" 183 | style.height = "\(height)px" 184 | _canvas.resize(width, height, pixelScale) 185 | _handleResize(Vector.new(width, height), pixelScale) 186 | } 187 | 188 | def operatingSystem UI.OperatingSystem { 189 | return _operatingSystem 190 | } 191 | 192 | def userAgent UI.UserAgent { 193 | return _userAgent 194 | } 195 | 196 | @neverinline 197 | def nowInSeconds double { 198 | return (window.performance ? window.performance : dynamic.Date).now() / 1000 199 | } 200 | 201 | def createWindow UI.Window { 202 | var window = _window 203 | _window = null # The browser lacks the ability to create additional windows 204 | return window 205 | } 206 | 207 | def _createCanvas CanvasElement { 208 | # Attempt to use WebGL first because it's a lot faster 209 | try { 210 | return CanvasElementWebGL.new(self) 211 | } 212 | 213 | # Fall back to the 2D canvas API 214 | return CanvasElement2D.new(self) 215 | } 216 | 217 | def _prepareElement { 218 | var style = element.style 219 | style.position = "relative" 220 | style.overflow = "hidden" 221 | _element.appendChild(_canvas.element) 222 | 223 | _mousemove = event => { 224 | dispatchEvent(_mouseEventFromEvent(.MOUSE_MOVE, event, null)) 225 | } 226 | 227 | _mouseup = event => { 228 | dispatchEvent(_mouseEventFromEvent(.MOUSE_UP, event, null)) 229 | _changeDragHandlers(.LOCAL) 230 | _draggingView = null 231 | } 232 | 233 | on(_element, "mousedown", event => { 234 | event.preventDefault() 235 | _setHasFocus(true) 236 | _draggingView = null 237 | _draggingView = dispatchEvent(_mouseEventFromEvent(.MOUSE_DOWN, event, null)) 238 | _changeDragHandlers(.GLOBAL) 239 | }) 240 | 241 | on(_element, "contextmenu", event => { 242 | event.preventDefault() 243 | }) 244 | 245 | on(_element, "wheel", event => { 246 | # Pinch-to-zoom in Chrome generates scroll events with the control key 247 | if event.ctrlKey { 248 | return 249 | } 250 | 251 | # Scroll deltas in Firefox are too small unless we handle deltaMode 252 | var deltaX = event.deltaX 253 | var deltaY = event.deltaY 254 | var deltaMode DeltaMode = event.deltaMode 255 | var scale = 256 | deltaMode == .PAGE ? size.y : 257 | deltaMode == .LINE ? 16 : 258 | 1 259 | 260 | dispatchEvent(_mouseEventFromEvent(.MOUSE_SCROLL, event, Vector.new(deltaX * scale, deltaY * scale))) 261 | event.preventDefault() 262 | }) 263 | 264 | _changeDragHandlers(.LOCAL) 265 | } 266 | 267 | def _mouseEventFromEvent(type UI.EventType, event dynamic, delta Vector) UI.MouseEvent { 268 | var bounds = _element.getBoundingClientRect() 269 | var locationInWindow = Vector.new(event.pageX - bounds.left, event.pageY - bounds.top) 270 | var target = _draggingView ?? viewFromLocation(locationInWindow) 271 | return UI.MouseEvent.new(type, target, locationInWindow, _modifiersFromEvent(event), event.detail as int, delta) 272 | } 273 | 274 | def _setHasFocus(hasFocus bool) { 275 | if hasFocus { 276 | _input.enabled = true 277 | _input.value = "" 278 | _input.focus() 279 | } 280 | 281 | else { 282 | _input.blur() 283 | _input.enabled = false 284 | } 285 | 286 | _updateIsActive 287 | } 288 | 289 | def _updateIsActive { 290 | _isActive = document.activeElement == _input && !document.hidden && _windowHasFocus 291 | } 292 | 293 | def _createInput { 294 | # Make the transparent and give it a width of 0. The must 295 | # be moved near the cursor so IME UI makes sense (completion list, etc.). 296 | var style = _input.style 297 | style.position = "absolute" 298 | style.border = "none" 299 | style.outline = "none" 300 | style.opacity = "0" 301 | style.padding = "0" 302 | style.transform = "scaleX(0)" # Safari breaks if we set "width" to 0 303 | style.left = "0" 304 | style.top = "0" 305 | 306 | on(_input, "blur", event => { 307 | _setHasFocus(false) 308 | }) 309 | 310 | on(_input, "keydown", event => { 311 | var modifiers = _modifiersFromEvent(event) 312 | var action = _shortcuts.get(_keyCodeFromEvent(event), modifiers) 313 | 314 | # Safari/IE have a broken clipboard implementation. Fall back to the 315 | # fake clipboard for cut and copy in those browsers. There's no way 316 | # to do feature detection for this bug unfortunately. 317 | if action == .CUT || action == .COPY { 318 | if _cutAndCopyAreProbablyBroken { 319 | var clipboardEvent = UI.ClipboardEvent.new(action == .CUT ? .CLIPBOARD_CUT : .CLIPBOARD_COPY, viewWithFocus, _fakeClipboard) 320 | dispatchEvent(clipboardEvent) 321 | _fakeClipboard = clipboardEvent.text 322 | } 323 | } 324 | 325 | # Ignore the paste action (we use the "paste" event instead) 326 | else if action != .NONE && action != .PASTE { 327 | _delegate?.triggerAction(action) 328 | event.preventDefault() 329 | } 330 | 331 | # Newlines won't appear as input events in a single-line 332 | else if event.which == '\r' { 333 | _insertText("\n", false) 334 | event.preventDefault() 335 | } 336 | }) 337 | 338 | # These events is supported by all major browsers 339 | var isComposing = false 340 | on(_input, "compositionstart", event => isComposing = true) 341 | on(_input, "compositionend", event => isComposing = false) 342 | 343 | # Text input events 344 | on(_input, "input", event => _insertText(_input.value, isComposing)) 345 | on(_input, "cut", event => _handleClipboardEvent(event, .CLIPBOARD_CUT)) 346 | on(_input, "copy", event => _handleClipboardEvent(event, .CLIPBOARD_COPY)) 347 | on(_input, "paste", event => _handleClipboardEvent(event, .CLIPBOARD_PASTE)) 348 | 349 | element.appendChild(_input) 350 | } 351 | 352 | def _insertText(text string, isComposing bool) { 353 | dispatchEvent(UI.TextEvent.new(.TEXT, viewWithFocus, text, isComposing)) 354 | if !isComposing { 355 | _input.value = "" 356 | } 357 | } 358 | 359 | def _handleClipboardEvent(event dynamic, type UI.EventType) { 360 | var normalClipboard = event.clipboardData 361 | var ieClipboard = window.clipboardData 362 | var text = _fakeClipboard 363 | 364 | # Load clipboard data 365 | if normalClipboard { 366 | text = normalClipboard.getData("text/plain") 367 | } else if ieClipboard { 368 | text = ieClipboard.getData("Text") 369 | } 370 | 371 | # If cut/copy don't work and we just pasted the same thing, read from the 372 | # fake clipboard instead of the real one because that may contain content 373 | # that was cut or copied and that didn't make it back to the real clipboard 374 | if _cutAndCopyAreProbablyBroken { 375 | if type == .CLIPBOARD_PASTE && text == _previousClipboard { 376 | text = _fakeClipboard 377 | } else { 378 | _previousClipboard = text 379 | _fakeClipboard = text 380 | } 381 | } 382 | 383 | # Dispatch the event to the view with focus 384 | var clipboardEvent = UI.ClipboardEvent.new(type, viewWithFocus, text) 385 | dispatchEvent(clipboardEvent) 386 | 387 | # Save clipboard data 388 | if clipboardEvent.text != text { 389 | if normalClipboard { 390 | normalClipboard.setData("text/plain", clipboardEvent.text) 391 | } else if ieClipboard { 392 | ieClipboard.setData("Text", clipboardEvent.text) 393 | } 394 | _fakeClipboard = clipboardEvent.text 395 | } 396 | 397 | # Make sure that this event doesn't actually insert into the input element 398 | event.preventDefault() 399 | } 400 | 401 | def _startAnimationLoop { 402 | var tick fn() = => { 403 | _delegate?.triggerFrame 404 | 405 | # Chrome sometimes automatically scrolls stuff that shouldn't be scrollable :( 406 | _element.scrollLeft = 0 407 | _element.scrollTop = 0 408 | 409 | # Only draw if requested 410 | if _isInvalid { 411 | render 412 | _isInvalid = false 413 | } 414 | 415 | (window.requestAnimationFrame ? window.requestAnimationFrame : dynamic.setTimeout)(tick) 416 | } 417 | tick() 418 | } 419 | 420 | enum ChangeDragHandlers { 421 | LOCAL 422 | GLOBAL 423 | } 424 | 425 | def _changeDragHandlers(mode ChangeDragHandlers) { 426 | var old = mode == .GLOBAL ? _element : document 427 | var new = mode == .GLOBAL ? document : _element 428 | 429 | off(old, "mousemove", _mousemove) 430 | off(old, "mouseup", _mouseup) 431 | 432 | on(new, "mousemove", _mousemove) 433 | on(new, "mouseup", _mouseup) 434 | } 435 | } 436 | 437 | namespace Window { 438 | def _modifiersFromEvent(event dynamic) UI.Modifiers { 439 | return 440 | (event.altKey ? .ALT : 0) | 441 | (event.metaKey ? .META : 0) | 442 | (event.shiftKey ? .SHIFT : 0) | 443 | (event.ctrlKey ? .CONTROL : 0) 444 | } 445 | 446 | def _keyCodeFromEvent(event dynamic) UI.Key { 447 | var which = event.which as int 448 | 449 | if which >= 'A' && which <= 'Z' { 450 | return (UI.Key.LETTER_A - 'A' + which) as UI.Key 451 | } 452 | 453 | if which >= '0' && which <= '9' { 454 | return (UI.Key.NUMBER_0 - '0' + which) as UI.Key 455 | } 456 | 457 | return _whichToKeyCode.get(event.which as int, .NONE) 458 | } 459 | } 460 | 461 | @entry if BUILD == .WWW 462 | def main { 463 | var window = Window.new 464 | window.stretchToFitBody 465 | Editor.App.new(window) 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /src/editor/action.sk: -------------------------------------------------------------------------------- 1 | namespace Editor { 2 | enum Action { 3 | NONE 4 | 5 | CUT 6 | COPY 7 | PASTE 8 | UNDO 9 | REDO 10 | 11 | SELECT_ALL 12 | SELECT_BREAK_INTO_LINES 13 | SELECT_EXPAND_TO_LINE 14 | SELECT_FIRST_REGION 15 | SELECT_NEXT_DIAGNOSTIC 16 | SELECT_PREVIOUS_DIAGNOSTIC 17 | SELECT_TOGGLE 18 | 19 | INSERT_CURSOR_ABOVE 20 | INSERT_CURSOR_BELOW 21 | INSERT_LINE_ABOVE 22 | INSERT_LINE_BELOW 23 | INSERT_TAB_BACKWARD 24 | INSERT_TAB_FORWARD 25 | 26 | SCROLL_DOWN_DOCUMENT 27 | SCROLL_DOWN_LINE 28 | SCROLL_UP_DOCUMENT 29 | SCROLL_UP_LINE 30 | 31 | MOVE_DOWN_DOCUMENT 32 | MOVE_DOWN_LINE 33 | MOVE_DOWN_PAGE 34 | MOVE_LEFT_CHARACTER 35 | MOVE_LEFT_LINE 36 | MOVE_LEFT_WORD 37 | MOVE_RIGHT_CHARACTER 38 | MOVE_RIGHT_LINE 39 | MOVE_RIGHT_WORD 40 | MOVE_UP_DOCUMENT 41 | MOVE_UP_LINE 42 | MOVE_UP_PAGE 43 | 44 | SELECT_DOWN_DOCUMENT 45 | SELECT_DOWN_LINE 46 | SELECT_DOWN_PAGE 47 | SELECT_LEFT_CHARACTER 48 | SELECT_LEFT_LINE 49 | SELECT_LEFT_WORD 50 | SELECT_RIGHT_CHARACTER 51 | SELECT_RIGHT_LINE 52 | SELECT_RIGHT_WORD 53 | SELECT_UP_DOCUMENT 54 | SELECT_UP_LINE 55 | SELECT_UP_PAGE 56 | 57 | DELETE_DOWN_DOCUMENT 58 | DELETE_DOWN_LINE 59 | DELETE_DOWN_PAGE 60 | DELETE_LEFT_CHARACTER 61 | DELETE_LEFT_LINE 62 | DELETE_LEFT_WORD 63 | DELETE_RIGHT_CHARACTER 64 | DELETE_RIGHT_LINE 65 | DELETE_RIGHT_WORD 66 | DELETE_UP_DOCUMENT 67 | DELETE_UP_LINE 68 | DELETE_UP_PAGE 69 | 70 | def isMoveMotion bool { 71 | return self >= MOVE_DOWN_DOCUMENT && self <= MOVE_UP_PAGE 72 | } 73 | 74 | def isSelectMotion bool { 75 | return self >= SELECT_DOWN_DOCUMENT && self <= SELECT_UP_PAGE 76 | } 77 | 78 | def isDeleteMotion bool { 79 | return self >= DELETE_DOWN_DOCUMENT && self <= DELETE_UP_PAGE 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/editor/app.sk: -------------------------------------------------------------------------------- 1 | namespace Editor { 2 | class App { 3 | const _platform UI.Platform 4 | 5 | def new(platform UI.Platform) { 6 | _platform = platform 7 | createWindow 8 | } 9 | 10 | def createWindow { 11 | WindowController.new(_platform) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/editor/buffer.sk: -------------------------------------------------------------------------------- 1 | namespace Editor { 2 | const INVALID_ADVANCE_WIDTH = -1.0 3 | 4 | enum CharacterClass { 5 | WORD 6 | OTHER 7 | SPACE 8 | } 9 | 10 | # For syntax highlighting, each line stores a sorted list of styled spans 11 | # produced by the lexer. The lexer state is also stored with the line so the 12 | # lexer can easily be resumed when the line changes without scanning down the 13 | # whole document. Lexing can also stop without scanning down the entire file 14 | # if the lexer state becomes the same again. 15 | class Line { 16 | const text string 17 | var advanceWidth = INVALID_ADVANCE_WIDTH 18 | var previousState LexerState = null 19 | var nextState LexerState = null 20 | var spans List = null 21 | var diagnostics List = null 22 | var hasErrors = false 23 | 24 | def isWordBoundary(index int, direction Direction) bool { 25 | var count = text.count 26 | assert(index >= 0 && index <= count) 27 | if index == 0 { return direction == .PREVIOUS } 28 | if index == count { return direction != .PREVIOUS } 29 | var left = classify(text[index - 1]) 30 | var right = classify(text[index]) 31 | return direction == .PREVIOUS ? left > right : left < right 32 | } 33 | 34 | def seekToBoundary(index int, direction Direction, step StepX) int { 35 | # Jump to the start or end of the line 36 | if step == .LINE { 37 | return direction == .PREVIOUS ? 0 : text.count 38 | } 39 | 40 | # Scan for the character or word boundary 41 | var iterator = Unicode.StringIterator.INSTANCE.reset(text, index) 42 | while true { 43 | var codePoint = direction == .PREVIOUS ? iterator.previousCodePoint : iterator.nextCodePoint 44 | if step == .CHARACTER || codePoint == -1 || isWordBoundary(iterator.index, direction) { 45 | return iterator.index 46 | } 47 | } 48 | } 49 | } 50 | 51 | namespace Line { 52 | def classify(c int) CharacterClass { 53 | if Lexer.isSpace(c) { return .SPACE } 54 | if Lexer.isAlphaOrDigit(c) { return .WORD } 55 | return .OTHER 56 | } 57 | 58 | def split(text string) List { 59 | var lines List = [] 60 | for part in text.split("\n") { 61 | lines.append(Line.new(part)) 62 | } 63 | return lines 64 | } 65 | 66 | def join(lines List) string { 67 | var text = "" 68 | for i in 0..lines.count { 69 | if i != 0 { 70 | text += "\n" 71 | } 72 | text += lines[i].text 73 | } 74 | return text 75 | } 76 | 77 | def maxAdvanceWidth(lines List) double { 78 | var result = 0.0 79 | for line in lines { 80 | assert(line.advanceWidth != INVALID_ADVANCE_WIDTH) 81 | result = Math.max(result, line.advanceWidth) 82 | } 83 | return result 84 | } 85 | } 86 | 87 | # This is the storage object for a document. It provides access to an ordered 88 | # collection of lines and tracks the maximum line advance width, which is 89 | # useful for scroll bounds. Each model object contains one line buffer. This 90 | # should probably be implemented using a gap buffer at some point for performance. 91 | class LineBuffer { 92 | var _lines = [Line.new("")] 93 | var _maxAdvanceWidth = 0.0 94 | 95 | def new { 96 | _lines[0].advanceWidth = 0 97 | } 98 | 99 | def toString string { 100 | return Line.join(_lines) 101 | } 102 | 103 | def count int { 104 | return _lines.count 105 | } 106 | 107 | def [](index int) Line { 108 | assert(0 <= index && index < count) 109 | return _lines[index] 110 | } 111 | 112 | def slice(start int, end int) List { 113 | assert(0 <= start && start <= end && end <= count) 114 | return _lines.slice(start, end) 115 | } 116 | 117 | def changeLines(start int, end int, lines List) { 118 | assert(lines.all(line => line.advanceWidth != INVALID_ADVANCE_WIDTH)) 119 | 120 | # Invalidate the maximum advance width 121 | if _maxAdvanceWidth != INVALID_ADVANCE_WIDTH { 122 | var size = Line.maxAdvanceWidth(lines) 123 | if size > _maxAdvanceWidth { 124 | _maxAdvanceWidth = size 125 | } else if Line.maxAdvanceWidth(_lines.slice(start, end)) == _maxAdvanceWidth { 126 | _maxAdvanceWidth = INVALID_ADVANCE_WIDTH 127 | } 128 | } 129 | 130 | # Replace all lines in the range [start, end) 131 | _lines.removeRange(start, end) 132 | _lines.insert(start, lines) # TODO: This is O(n^2) 133 | } 134 | 135 | def maxAdvanceWidth double { 136 | if _maxAdvanceWidth == INVALID_ADVANCE_WIDTH { 137 | _maxAdvanceWidth = Line.maxAdvanceWidth(_lines) 138 | } 139 | return _maxAdvanceWidth 140 | } 141 | 142 | def clearStyleState { 143 | for line in _lines { 144 | line.previousState = null 145 | line.nextState = null 146 | line.spans = null 147 | } 148 | } 149 | 150 | def replaceDiagnostics(diagnostics IntMap>) { 151 | for i in 0.._lines.count { 152 | var line = _lines[i] 153 | var values = diagnostics.get(i, null) 154 | 155 | line.diagnostics = values 156 | line.hasErrors = false 157 | 158 | if values != null { 159 | for value in values { 160 | if value.kind == .ERROR { 161 | line.hasErrors = true 162 | break 163 | } 164 | } 165 | } 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/editor/change.sk: -------------------------------------------------------------------------------- 1 | namespace Editor { 2 | enum ChangeEffect { 3 | NONE 4 | INSERT 5 | DELETE 6 | REPLACE 7 | } 8 | 9 | namespace ChangeEffect { 10 | def fromDeleteAndInsert(isDelete bool, isInsert bool) ChangeEffect { 11 | return ((isDelete ? DELETE : NONE) | (isInsert ? INSERT : NONE)) as ChangeEffect 12 | } 13 | } 14 | 15 | # Holds a single, continuous edit operation. A pure deletion is a change with 16 | # a non-empty region and empty text, while a pure insertion operation is a 17 | # change with an empty region and non-empty text. Changes are mutated during 18 | # undoing and redoing to hold the inverse of the operation performed. 19 | class Change { 20 | var region Region 21 | var text string 22 | 23 | def toString string { 24 | return "Change(\(region), '\(text)')" 25 | } 26 | 27 | def effect ChangeEffect { 28 | return ChangeEffect.fromDeleteAndInsert(!region.isEmpty, text != "") 29 | } 30 | } 31 | 32 | namespace Change { 33 | def joinInserts(first List, second List) List { 34 | if first.count != second.count { 35 | return null 36 | } 37 | 38 | var changes List = [] 39 | var deltaX = 0 40 | var deltaY = 0 41 | var oldY = 0 42 | 43 | for i in 0..first.count { 44 | var a = first[i] 45 | var b = second[i] 46 | var maxA = a.region.max 47 | var minB = b.region.min 48 | var lines = a.text.split("\n") 49 | var count = lines.count 50 | var maxAX = (count == 1 ? maxA.x : 0) + lines[count - 1].count + (maxA.y == oldY ? deltaX : 0) 51 | var maxAY = maxA.y + count - 1 + deltaY 52 | 53 | # Two inserts 54 | if a.effect != .INSERT || b.effect != .INSERT || maxAX != minB.x || maxAY != minB.y { 55 | return null 56 | } 57 | 58 | # Join the changes 59 | changes.append(Change.new(a.region, a.text + b.text)) 60 | 61 | # Store the delta for the next iteration 62 | deltaX = maxAX - maxA.x 63 | deltaY = maxAY - maxA.y 64 | oldY = maxA.y 65 | } 66 | 67 | return changes 68 | } 69 | 70 | def joinDeletes(first List, second List) List { 71 | var changes List = [] 72 | var deltaX = 0 73 | var deltaY = 0 74 | var oldY = 0 75 | 76 | for i in 0..first.count { 77 | var a = first[i] 78 | var b = second[i] 79 | var minA = a.region.min 80 | var maxA = a.region.max 81 | var minB = b.region.min 82 | var maxB = b.region.max 83 | var minAX = minA.x + (minA.y == oldY ? deltaX : 0) 84 | var minAY = minA.y + deltaY 85 | var maxAX = maxA.x + (maxA.y == oldY ? deltaX : 0) 86 | var maxAY = maxA.y + deltaY 87 | var effectA = a.effect 88 | 89 | # Two deletes 90 | if effectA != .DELETE && effectA != .REPLACE || b.effect != .DELETE || maxAX != minB.x || maxAY != minB.y { 91 | return null 92 | } 93 | 94 | # Join the changes 95 | changes.append(Change.new(Region.new(Marker.new(minAX, minAY), maxB), a.text)) 96 | 97 | # Store the delta for the next iteration 98 | deltaX = minB.y == maxB.y ? deltaX + maxB.x - minB.x : maxB.x 99 | deltaY += maxB.y - minB.y 100 | oldY = maxA.y 101 | } 102 | 103 | return changes 104 | } 105 | 106 | def isSpace(changes List) bool { 107 | var result = false 108 | for change in changes { 109 | for i in 0..change.text.count { 110 | var c = change.text[i] 111 | if c != ' ' && c != '\t' && c != '\n' { 112 | return false 113 | } 114 | result = true 115 | } 116 | } 117 | return result 118 | } 119 | } 120 | 121 | # Holds a set of changes and the original timestamp from when those changes 122 | # were first applied. Changes are applied in sets instead of individually 123 | # for performance. 124 | class Commit { 125 | var changes List 126 | var timestampInSeconds double 127 | var isSpace bool 128 | 129 | def toString string { 130 | var joined = ", ".join(changes.map(c => c.toString)) 131 | return "Commit([\(joined)], \(timestampInSeconds))" 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/editor/diagnostic.sk: -------------------------------------------------------------------------------- 1 | namespace Editor { 2 | enum DiagnosticKind { 3 | ERROR 4 | WARNING 5 | } 6 | 7 | enum RequestStatus { 8 | READY 9 | BUSY 10 | OUTDATED 11 | } 12 | 13 | # A warning or error on a single line. Multi-line diagnostics just show up 14 | # in the editor as a single diagnostic on the first line. 15 | class Diagnostic { 16 | const kind DiagnosticKind 17 | const source string 18 | const line int # 0-based line 19 | const start int # 0-based column 20 | const end int # 0-based column 21 | const text string 22 | } 23 | 24 | namespace Diagnostic { 25 | def extractDiagnosticsWithSource(diagnostics List, name string) IntMap> { 26 | var map = IntMap>.new 27 | for diagnostic in diagnostics { 28 | if diagnostic.source == name { 29 | var lines = map.get(diagnostic.line, null) 30 | if lines == null { 31 | lines = [] 32 | map[diagnostic.line] = lines 33 | } 34 | lines.append(diagnostic) 35 | } 36 | } 37 | return map 38 | } 39 | } 40 | 41 | interface DiagnosticObserver { 42 | def handleDiagnosticsChange(task DiagnosticTask, diagnostics List) 43 | } 44 | 45 | # This is the base class for an asynchronous task that produces diagnostics 46 | # in response to code changes. It includes support for ignoring outdated 47 | # diagnostics due to code changes since an asynchronous request was started. 48 | class DiagnosticTask { 49 | def changeFileContents(name string, contents string) 50 | def removeFile(name string) 51 | 52 | def addObserver(observer DiagnosticObserver) { 53 | _observers.appendOne(observer) 54 | } 55 | 56 | def removeObserver(observer DiagnosticObserver) { 57 | _observers.removeOne(observer) 58 | } 59 | 60 | def _handleDiagnosticsChange(diagnostics List) { 61 | for observer in _observers { 62 | observer.handleDiagnosticsChange(self, diagnostics) 63 | } 64 | } 65 | 66 | def _tryToStartRequest bool { 67 | if _requestStatus == .READY { 68 | _requestStatus = .BUSY 69 | return true 70 | } 71 | 72 | return false 73 | } 74 | 75 | def _tryToFinishRequest bool { 76 | if _requestStatus != .OUTDATED { 77 | _requestStatus = .READY 78 | return true 79 | } 80 | 81 | _requestStatus = .READY 82 | return false 83 | } 84 | 85 | def _invalidatePendingRequest { 86 | if _requestStatus == .BUSY { 87 | _requestStatus = .OUTDATED 88 | } 89 | } 90 | 91 | var _observers List = [] 92 | var _requestStatus RequestStatus = .READY 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/editor/lexer.sk: -------------------------------------------------------------------------------- 1 | namespace Editor { 2 | class Span { 3 | const start int 4 | const end int 5 | const offsetX double 6 | const advanceWidth double 7 | const color UI.Color 8 | } 9 | 10 | namespace Span { 11 | const ORDER_BY_START = (a Span, b Span) => a.start <=> b.start 12 | } 13 | 14 | class LexerState { 15 | def equals(other LexerState) bool { 16 | return other == self 17 | } 18 | } 19 | 20 | # A resumable lexer that styles lines of text. Custom lexers can easily be 21 | # created by creating a new Lexer. There is an optional state system if 22 | # you need to track information across lines. For example, a multi-line 23 | # comment would use a different lexer state for the next line than a single- 24 | # line comment. For simple enum-style state, just creating a fixed number of 25 | # LexerState objects and check for equality. More advanced usage can be done 26 | # by subclassing LexerState, adding extra fields, and overriding equals(). 27 | class Lexer { 28 | const _tokenizeLine fn(Lexer) 29 | var _spans List = null 30 | var _state LexerState = null 31 | var _current Line = null 32 | var _currentIndex = 0 33 | var _nextCodePoint = 0 34 | var _limit = 0 35 | var _startOfState = 0 36 | var _iterator = UI.AdvanceWidthIterator.new 37 | 38 | def styleLine(line Line, previousState LexerState) { 39 | _state = previousState 40 | _iterator.reset(line.text) 41 | _current = line 42 | _currentIndex = 0 43 | _nextCodePoint = -1 44 | _startOfState = 0 45 | _limit = line.text.count 46 | _spans = [] 47 | _tokenizeLine(self) 48 | 49 | # Make sure all tabs have spans since they are variable width 50 | var previousIndex = 0 51 | var count = _spans.count 52 | for i in 0..count { 53 | var span = _spans[i] 54 | assert(previousIndex <= span.start) # Tokenized spans must be sorted 55 | _addSpansForTabs(previousIndex, span.start) 56 | previousIndex = span.end 57 | } 58 | _addSpansForTabs(previousIndex, _limit) 59 | if _spans.count != count { 60 | _spans.sort(Span.ORDER_BY_START) 61 | } 62 | 63 | line.previousState = previousState 64 | line.nextState = _state 65 | line.spans = _spans 66 | assert(_state != null) 67 | } 68 | 69 | def setFont(font UI.FontInstance, indent int) { 70 | _iterator.setFont(font, indent) 71 | } 72 | 73 | def currentText string { 74 | return _current.text 75 | } 76 | 77 | def currentIndex int { 78 | return _currentIndex 79 | } 80 | 81 | def currentState LexerState { 82 | return _state 83 | } 84 | 85 | def endOfLine int { 86 | return _limit 87 | } 88 | 89 | def hasNext bool { 90 | return _currentIndex < _limit 91 | } 92 | 93 | def next { 94 | assert(_iterator.currentIndex >= _currentIndex) 95 | if _currentIndex == _iterator.currentIndex { 96 | _iterator.nextCodePoint 97 | } 98 | _currentIndex = _iterator.currentIndex 99 | } 100 | 101 | def startOfState int { 102 | return _startOfState 103 | } 104 | 105 | def peekNext int { 106 | _loadNextIfNeeded 107 | return _nextCodePoint 108 | } 109 | 110 | def takeNext int { 111 | var c = peekNext 112 | next 113 | return c 114 | } 115 | 116 | def matchNext(c int) bool { 117 | if peekNext == c { 118 | next 119 | return true 120 | } 121 | return false 122 | } 123 | 124 | def scanAlphaNumericString string { 125 | var text = "" 126 | while hasNext { 127 | var c = peekNext 128 | if !Lexer.isAlphaOrDigit(c) { 129 | break 130 | } 131 | next 132 | text += string.fromCodeUnit(c) 133 | } 134 | return text 135 | } 136 | 137 | def transitionToState(state LexerState, startingIndex int) { 138 | _state = state 139 | _startOfState = startingIndex 140 | } 141 | 142 | def addSpan(start int, end int, color UI.Color) { 143 | assert(0 <= start && start < end && end <= _limit) 144 | 145 | # Get the start position 146 | _iterator.seekToIndex(start) 147 | var previousIndex = start 148 | var previousX = _iterator.advanceWidthFromLeft 149 | 150 | # Split the span at each tab character 151 | while _iterator.currentIndex < end { 152 | var nextIndex = _iterator.currentIndex 153 | var nextX = _iterator.advanceWidthFromLeft 154 | var codePoint = _iterator.nextCodePoint 155 | 156 | # Tab width depends on context so separate it out into its own span 157 | if codePoint == '\t' { 158 | _spans.append(Span.new(previousIndex, nextIndex, previousX, nextX - previousX, color)) 159 | previousIndex = _iterator.currentIndex 160 | previousX = _iterator.advanceWidthFromLeft 161 | _spans.append(Span.new(nextIndex, previousIndex, nextX, previousX - nextX, color)) 162 | } 163 | } 164 | 165 | # Add a final span at the end 166 | if previousIndex < end { 167 | _spans.append(Span.new(previousIndex, _iterator.currentIndex, previousX, _iterator.advanceWidthFromLeft - previousX, color)) 168 | } 169 | 170 | # Don't disrupt lexing 171 | _iterator.seekToIndex(_currentIndex) 172 | } 173 | 174 | def changePreviousSpanColor(color UI.Color) { 175 | var previous = _spans.last 176 | _spans.last = Span.new(previous.start, previous.end, previous.offsetX, previous.advanceWidth, color) 177 | } 178 | 179 | def _loadNextIfNeeded { 180 | assert(_iterator.currentIndex >= _currentIndex) 181 | if _iterator.currentIndex == _currentIndex { 182 | _nextCodePoint = _iterator.nextCodePoint 183 | } 184 | } 185 | 186 | def _addSpansForTabs(start int, end int) { 187 | var line = _current 188 | for i in start..end { 189 | if line.text[i] == '\t' { 190 | addSpan(i, i + 1, .FOREGROUND_DEFAULT) 191 | } 192 | } 193 | } 194 | } 195 | 196 | namespace Lexer { 197 | def isSpace(c int) bool { 198 | return c == ' ' || c == '\t' 199 | } 200 | 201 | def isDigit(c int) bool { 202 | return c >= '0' && c <= '9' 203 | } 204 | 205 | def isUpperCase(c int) bool { 206 | return c >= 'A' && c <= 'Z' 207 | } 208 | 209 | def isLowerCase(c int) bool { 210 | return c >= 'a' && c <= 'z' 211 | } 212 | 213 | def isAlpha(c int) bool { 214 | return isUpperCase(c) || isLowerCase(c) || c == '_' 215 | } 216 | 217 | def isAlphaOrDigit(c int) bool { 218 | return isAlpha(c) || isDigit(c) 219 | } 220 | 221 | def hasLowerCase(text string) bool { 222 | for i in 0..text.count { 223 | if isLowerCase(text[i]) { 224 | return true 225 | } 226 | } 227 | return false 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/editor/model.sk: -------------------------------------------------------------------------------- 1 | namespace Editor { 2 | enum CommitDirection { 3 | UNDO 4 | REDO 5 | } 6 | 7 | interface ModelObserver { 8 | def handleLexerChange(model Model) 9 | def handleToggleCommit(model Model, commit Commit, direction CommitDirection) 10 | def handleDiagnosticChange(model Model) 11 | } 12 | 13 | # Wraps a LineBuffer and manages undo/redo and update events. Also keeps the 14 | # syntax highlighting information up to date by running a lexer as needed. 15 | # The model is separate from the view so one model can have multiple views. 16 | class Model { 17 | var _name string 18 | var _platform UI.Platform 19 | var _font UI.FontInstance 20 | var _currentIndent int 21 | var _lexer Lexer = null 22 | var _observers List = [] 23 | var _commits List = [] 24 | var _task DiagnosticTask = null 25 | var _lines = LineBuffer.new 26 | var _commitIndex = 0 27 | var _dirtyIndex = 0 28 | 29 | def toString string { 30 | return _lines.toString 31 | } 32 | 33 | def count int { 34 | return _lines.count 35 | } 36 | 37 | def font UI.FontInstance { 38 | return _font 39 | } 40 | 41 | def maxAdvanceWidth double { 42 | return _lines.maxAdvanceWidth 43 | } 44 | 45 | def currentIndent int { 46 | return _currentIndent 47 | } 48 | 49 | def [](index int) Line { 50 | return _lines[index] 51 | } 52 | 53 | def addObserver(observer ModelObserver) { 54 | _observers.appendOne(observer) 55 | } 56 | 57 | def removeObserver(observer ModelObserver) { 58 | _observers.removeOne(observer) 59 | } 60 | 61 | def canUndo bool { 62 | return _commitIndex > 0 63 | } 64 | 65 | def canRedo bool { 66 | return _commitIndex < _commits.count 67 | } 68 | 69 | def undo { 70 | if canUndo { 71 | _commitIndex-- 72 | _toggleCommit(_commits[_commitIndex], .UNDO) 73 | } 74 | } 75 | 76 | def redo { 77 | if canRedo { 78 | _toggleCommit(_commits[_commitIndex], .REDO) 79 | _commitIndex++ 80 | } 81 | } 82 | 83 | def setLexer(lexer Lexer) { 84 | if _lexer != lexer { 85 | _lexer = lexer 86 | _lexer.setFont(_font, _currentIndent) 87 | _dirtyIndex = 0 88 | _lines.clearStyleState 89 | for observer in _observers { 90 | observer.handleLexerChange(self) 91 | } 92 | } 93 | } 94 | 95 | def setDiagnosticTask(task DiagnosticTask) { 96 | if _task != task { 97 | _task = task 98 | if task != null { 99 | task.changeFileContents(_name, toString) 100 | } 101 | replaceDiagnostics([]) 102 | } 103 | } 104 | 105 | def replaceDiagnostics(diagnostics List) { 106 | _lines.replaceDiagnostics(Diagnostic.extractDiagnosticsWithSource(diagnostics, _name)) 107 | for observer in _observers { 108 | observer.handleDiagnosticChange(self) 109 | } 110 | } 111 | 112 | def styleLinesIfNeeded(start int, end int) { 113 | assert(0 <= start && start <= end && end <= count) 114 | 115 | if _lexer != null { 116 | var previousState = _dirtyIndex != 0 ? _lines[_dirtyIndex - 1].nextState : null 117 | var i = _dirtyIndex 118 | 119 | # Style all unstyled lines up to the end of the requested region 120 | while i < end { 121 | var line = _lines[i] 122 | if previousState == null || line.previousState == null || !previousState.equals(line.previousState) { 123 | _lexer.styleLine(line, previousState) 124 | } 125 | previousState = line.nextState 126 | i++ 127 | } 128 | 129 | # Lines are now only unstyled after this point 130 | _dirtyIndex = i 131 | } 132 | } 133 | 134 | def applyChanges(changes List) { 135 | if !changes.isEmpty { 136 | while _commits.count > _commitIndex { 137 | _commits.removeLast 138 | } 139 | _commits.append(Commit.new(changes, _platform.nowInSeconds, Change.isSpace(changes))) 140 | redo 141 | 142 | # Attempt to merge this commit onto the previous one after it's applied 143 | var count = _commits.count 144 | if count >= 2 { 145 | var merged = _mergeCommits(_commits[count - 2], _commits[count - 1]) 146 | if merged != null { 147 | _commits.removeRange(count - 2, count) 148 | _commits.append(merged) 149 | _commitIndex-- 150 | } 151 | } 152 | } 153 | } 154 | 155 | def slice(region Region) string { 156 | var min = region.min 157 | var max = region.max 158 | assert(0 <= min.y && max.y < _lines.count) 159 | assert(0 <= min.x && min.x <= _lines[min.y].text.count) 160 | assert(0 <= max.x && max.x <= _lines[max.y].text.count) 161 | var first = _lines[min.y] 162 | var last = _lines[max.y] 163 | if first == last { 164 | return first.text.slice(min.x, max.x) 165 | } 166 | var text = first.text.slice(min.x, first.text.count) 167 | for y = min.y + 1; y < max.y; y++ { 168 | text += "\n" + _lines[y].text 169 | } 170 | text += "\n" + last.text.slice(0, max.x) 171 | return text 172 | } 173 | 174 | def _toggleCommit(commit Commit, direction CommitDirection) { 175 | var deltaX = 0 176 | var deltaY = 0 177 | var oldX = 0 178 | var oldY = 0 179 | 180 | for change in commit.changes { 181 | # Get the region bounds, shifted due to previous changes 182 | var min = change.region.min 183 | var max = change.region.max 184 | var minX = min.x + (min.y == oldY ? deltaX : 0) 185 | var maxX = max.x + (max.y == oldY ? deltaX : 0) 186 | var minY = min.y + deltaY 187 | var maxY = max.y + deltaY 188 | oldX = max.x 189 | oldY = max.y 190 | 191 | # Extract the text in the region and generate the replacement text 192 | var oldLines = _lines.slice(minY, maxY + 1) 193 | var newLines = Line.split(change.text) 194 | var oldLinesSize = oldLines.count 195 | var newLinesSize = newLines.count 196 | var oldLast = oldLines[oldLinesSize - 1] 197 | var newLast = newLines[newLinesSize - 1] 198 | newLines[newLinesSize - 1] = Line.new(newLast.text + oldLast.text.slice(maxX, oldLast.text.count)) 199 | newLines[0] = Line.new(oldLines[0].text.slice(0, minX) + newLines[0].text) 200 | oldLines[oldLinesSize - 1] = Line.new(oldLast.text.slice(0, maxX)) 201 | oldLines[0] = Line.new(oldLines[0].text.slice(minX, oldLines[0].text.count)) 202 | 203 | # Update the document 204 | var iterator = UI.AdvanceWidthIterator.INSTANCE 205 | iterator.setFont(_font, _currentIndent) 206 | for line in newLines { 207 | iterator.reset(line.text) 208 | iterator.seekToIndex(line.text.count) 209 | line.advanceWidth = iterator.advanceWidthFromLeft 210 | } 211 | _lines.changeLines(minY, maxY + 1, newLines) 212 | 213 | # Mutate the change to represent the inverse operation 214 | maxX += newLines[newLinesSize - 1].text.count - oldLast.text.count 215 | maxY = minY + newLines.count - 1 216 | change.region = Region.new(Marker.new(minX, minY), Marker.new(maxX, maxY)) 217 | change.text = Line.join(oldLines) 218 | 219 | # Update the deltas for future changes 220 | deltaX = maxX - oldX 221 | deltaY = maxY - oldY 222 | 223 | # Track the dirty area for style updates 224 | _dirtyIndex = Math.min(_dirtyIndex, minY) 225 | } 226 | 227 | # Notify all observers 228 | for observer in _observers { 229 | observer.handleToggleCommit(self, commit, direction) 230 | } 231 | 232 | if _task != null { 233 | _task.changeFileContents(_name, toString) 234 | } 235 | } 236 | } 237 | 238 | namespace Model { 239 | const MAX_MERGE_DELTA = 0.5 240 | 241 | def _mergeCommits(first Commit, second Commit) Commit { 242 | if !first.isSpace && second.isSpace || # Break after each word 243 | first.changes.count != second.changes.count || # Must have similar selections 244 | second.timestampInSeconds - first.timestampInSeconds > MAX_MERGE_DELTA { 245 | return null 246 | } 247 | 248 | var timestampInSeconds = Math.max(first.timestampInSeconds, second.timestampInSeconds) 249 | var isSpace = first.isSpace && second.isSpace 250 | 251 | var inserts = Change.joinInserts(second.changes, first.changes) 252 | if inserts != null { 253 | return Commit.new(inserts, timestampInSeconds, isSpace) 254 | } 255 | 256 | var deletes = Change.joinDeletes(first.changes, second.changes) 257 | if deletes != null { 258 | return Commit.new(deletes, timestampInSeconds, isSpace) 259 | } 260 | 261 | return null 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/editor/scrollbar.sk: -------------------------------------------------------------------------------- 1 | namespace Editor { 2 | enum ScrollBehavior { 3 | DO_NOT_SCROLL 4 | SCROLL_INTO_VIEW 5 | CENTER_IF_OUT_OF_VIEW 6 | } 7 | 8 | # Each view has two scrollbar objects, one for the horizontal scrollbar and 9 | # one for the vertical scrollbar. The scrollbar size is independent from the 10 | # size of the viewport into the content. 11 | class Scrollbar { 12 | var _isX bool 13 | var _x = 0.0 14 | var _y = 0.0 15 | var _width = 0.0 16 | var _height = 0.0 17 | var _minSize = 0.0 18 | var _viewSize = 0.0 19 | var _viewOffset = 0.0 20 | var _scrollSize = 0.0 21 | var _scrollOffset = 0.0 22 | var _positionOffset = 0.0 23 | var _positionSize = 0.0 24 | var _mappedSize = 0.0 25 | var _mappedOffset = 0.0 26 | var _draggingOffset = 0.0 27 | var _isDraggingThumb = false 28 | 29 | def new(axis Axis) { 30 | _isX = axis == .X 31 | } 32 | 33 | def isNeeded bool { 34 | return _viewSize < _scrollSize 35 | } 36 | 37 | def viewSize double { 38 | return _viewSize 39 | } 40 | 41 | def scrollSize double { 42 | return _scrollSize 43 | } 44 | 45 | def scrollOffset double { 46 | return _scrollOffset 47 | } 48 | 49 | # Return a mapped position that will be at the center of the thumb when 50 | # the document is scrolled so this position is vertically centered 51 | def mappedPosition(position double) double { 52 | return _positionOffset + _mappedSize / 2 + (position - _viewSize / 2) * (_positionSize - _mappedSize) / (_scrollSize - _viewSize) 53 | } 54 | 55 | def setScrollOffset(scrollOffset double) bool { 56 | scrollOffset = Math.max(0, Math.min(scrollOffset, _scrollSize - _viewSize)) 57 | if _scrollOffset != scrollOffset { 58 | _scrollOffset = scrollOffset 59 | _computeMapped 60 | return true 61 | } 62 | return false 63 | } 64 | 65 | def setMinimumSize(minSize double) { 66 | _minSize = minSize 67 | _computeMapped 68 | } 69 | 70 | def setPosition(minX double, minY double, maxX double, maxY double) { 71 | _x = minX 72 | _y = minY 73 | _width = maxX - minX 74 | _height = maxY - minY 75 | _positionOffset = _isX ? _x : _y 76 | _positionSize = _isX ? _width : _height 77 | _computeMapped 78 | } 79 | 80 | def resize(viewSize double, scrollSize double) { 81 | if _viewSize != viewSize || _scrollSize != scrollSize { 82 | _viewSize = viewSize 83 | _scrollSize = scrollSize 84 | 85 | if !setScrollOffset(_scrollOffset) { 86 | _computeMapped 87 | } 88 | } 89 | } 90 | 91 | def render(renderer UI.SemanticRenderer) { 92 | if isNeeded { 93 | renderer.renderScrollbarThumb( 94 | _isX ? _x + _mappedOffset : _x, 95 | _isX ? _y : _y + _mappedOffset, 96 | _isX ? _mappedSize : _width, 97 | _isX ? _height : _mappedSize, 98 | .FOREGROUND_DEFAULT) 99 | } 100 | } 101 | 102 | def containsPoint(viewLocation Vector) bool { 103 | var x = viewLocation.x - _x 104 | var y = viewLocation.y - _y 105 | return isNeeded && x >= 0 && y >= 0 && x < _width && y < _height 106 | } 107 | 108 | def startDragging(viewLocation Vector, modifiers UI.Modifiers) bool { 109 | if containsPoint(viewLocation) { 110 | var pageSize = _viewSize * (_positionSize - _mappedSize) / (_scrollSize - _viewSize) 111 | _draggingOffset = .ALT in modifiers ? _mappedSize / 2 : (_isX ? viewLocation.x - _x : viewLocation.y - _y) - _mappedOffset 112 | _draggingOffset += pageSize * (_draggingOffset < 0 ? 1 : _draggingOffset >= _mappedSize ? -1 : 0) 113 | return true 114 | } 115 | return false 116 | } 117 | 118 | def continueDragging(viewLocation Vector) bool { 119 | return setScrollOffset(((_isX ? viewLocation.x - _x : viewLocation.y - _y) - _draggingOffset) * (_scrollSize - _viewSize) / (_positionSize - _mappedSize)) 120 | } 121 | 122 | def _computeMapped { 123 | _mappedSize = Math.max(_minSize, _positionSize * _viewSize / _scrollSize) 124 | _mappedOffset = (_positionSize - _mappedSize) * _scrollOffset / (_scrollSize - _viewSize) 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/editor/selection.sk: -------------------------------------------------------------------------------- 1 | namespace Editor { 2 | # The point (x, y) is the position of the marker in infinite, unwrapped 3 | # document space, where (0, 0) is the first character in the document. 4 | class Marker { 5 | const x int 6 | const y int 7 | 8 | # Moving a caret up or down past a blank line should end up at a similar 9 | # x offset value, not at an x offset value of 0. That original offset is 10 | # tracked here. 11 | const originalOffsetX double 12 | 13 | def new(x int, y int, originalOffsetX double) { 14 | self.x = x 15 | self.y = y 16 | self.originalOffsetX = originalOffsetX 17 | } 18 | 19 | def toString string { 20 | return "Marker(\(x), \(y), \(originalOffsetX))" 21 | } 22 | 23 | def equals(other Marker) bool { 24 | return x == other.x && y == other.y && originalOffsetX == other.originalOffsetX 25 | } 26 | 27 | def <=>(other Marker) int { 28 | var delta = y <=> other.y 29 | return delta != 0 ? delta : x <=> other.x 30 | } 31 | } 32 | 33 | namespace Marker { 34 | const ZERO = new(0, 0) 35 | const INVALID_OFFSET_X = -1 36 | 37 | def min(a Marker, b Marker) Marker { 38 | return a < b ? a : b 39 | } 40 | 41 | def max(a Marker, b Marker) Marker { 42 | return a > b ? a : b 43 | } 44 | 45 | def new(x int, y int) Marker { 46 | return new(x, y, INVALID_OFFSET_X) 47 | } 48 | } 49 | 50 | # A region stretches from min(start, end) inclusive to max(start, end) exclusive. 51 | # When selecting, a user will define start first as the anchor of the selection, 52 | # and then define end as the extent of the selection. It will be the case that 53 | # start is greater than end if the user is selecting backwards. 54 | class Region { 55 | const start Marker 56 | const end Marker 57 | 58 | def new(start Marker, end Marker) { 59 | self.start = start 60 | self.end = end 61 | } 62 | 63 | def toString string { 64 | return "Region(\(start), \(end))" 65 | } 66 | 67 | def min Marker { 68 | return Marker.min(start, end) 69 | } 70 | 71 | def max Marker { 72 | return Marker.max(start, end) 73 | } 74 | 75 | def isMultiline bool { 76 | return start.y != end.y 77 | } 78 | 79 | def isEmpty bool { 80 | return start.equals(end) 81 | } 82 | 83 | def in(marker Marker) bool { 84 | return min <= marker && marker < max 85 | } 86 | } 87 | 88 | namespace Region { 89 | const EMPTY = new(Marker.new(0, 0)) 90 | 91 | # Regions are sorted by their starting marker. The case where regions overlap 92 | # doesn't matter since selections will merge these regions immediately. 93 | const COMPARE = (first Region, second Region) => first.min <=> second.min 94 | 95 | def new(marker Marker) Region { 96 | return new(marker, marker) 97 | } 98 | 99 | def span(first Region, second Region) Region { 100 | var start = Marker.min(first.min, second.min) 101 | var end = Marker.max(first.max, second.max) 102 | return start.equals(first.start) || end.equals(second.end) ? new(start, end) : new(end, start) 103 | } 104 | } 105 | 106 | # An ordered collection of regions. This editor uses the multiple selection 107 | # model which lets users type in multiple locations at once. For example, 108 | # instead of having to use a find/replace dialog for simple string 109 | # substitutions, users can just select all occurrences simultaneously and 110 | # type the replacement in real time. 111 | class Selection { 112 | const regions List 113 | 114 | def new(regions List) { 115 | self.regions = regions 116 | regions.sort(Region.COMPARE) 117 | 118 | # Merge overlapping regions in place using an O(n) algorithm 119 | var i = 0 120 | var target = 0 121 | while i < regions.count { 122 | var previous = regions[i] 123 | while i + 1 < regions.count { 124 | var next = regions[i + 1] 125 | var comparison = previous.max <=> next.min 126 | if comparison <= 0 && (comparison != 0 || !previous.isEmpty && !next.isEmpty) { 127 | break 128 | } 129 | previous = Region.span(previous, next) 130 | i++ 131 | } 132 | regions[target] = previous 133 | target++ 134 | i++ 135 | } 136 | 137 | # Make sure the regions list is the right length 138 | while regions.count > target { 139 | regions.removeLast 140 | } 141 | } 142 | 143 | def min Marker { 144 | return regions.first.min 145 | } 146 | 147 | def max Marker { 148 | return regions.last.max 149 | } 150 | 151 | def isEmpty bool { 152 | for region in regions { 153 | if !region.isEmpty { 154 | return false 155 | } 156 | } 157 | return true 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/editor/shortcuts.sk: -------------------------------------------------------------------------------- 1 | namespace Editor { 2 | class Shortcut { 3 | const action Action 4 | const modifiers UI.Modifiers 5 | } 6 | 7 | class ShortcutMap { 8 | const _map IntMap> = {} 9 | 10 | def new(platform UI.Platform) { 11 | _load(self, platform) 12 | } 13 | 14 | def set(keyCode UI.Key, modifiers UI.Modifiers, action Action) { 15 | var shortcuts = _map.get(keyCode, null) 16 | if shortcuts == null { 17 | shortcuts = [] 18 | _map[keyCode] = shortcuts 19 | } 20 | shortcuts.append(Shortcut.new(action, modifiers)) 21 | } 22 | 23 | def get(keyCode UI.Key, modifiers UI.Modifiers) Action { 24 | var shortcuts = _map.get(keyCode, null) 25 | if shortcuts != null { 26 | for shortcut in shortcuts { 27 | if shortcut.modifiers == modifiers { 28 | return shortcut.action 29 | } 30 | } 31 | } 32 | return .NONE 33 | } 34 | } 35 | 36 | namespace ShortcutMap { 37 | def _load(map ShortcutMap, platform UI.Platform) { 38 | var base = platform.baseModifier 39 | var isOSX = platform.operatingSystem == .OSX 40 | 41 | map.set(.LETTER_A, base, .SELECT_ALL) 42 | map.set(.LETTER_C, base, .COPY) 43 | map.set(.LETTER_L, base | .SHIFT, .SELECT_BREAK_INTO_LINES) 44 | map.set(.LETTER_L, base, .SELECT_EXPAND_TO_LINE) 45 | map.set(.LETTER_V, base, .PASTE) 46 | map.set(.LETTER_X, base, .CUT) 47 | map.set(.LETTER_Y, base, .REDO) 48 | map.set(.LETTER_Z, base | .SHIFT, .REDO) 49 | map.set(.LETTER_Z, base, .UNDO) 50 | map.set(.ENTER, base | .SHIFT, .INSERT_LINE_ABOVE) 51 | map.set(.ENTER, base, .INSERT_LINE_BELOW) 52 | map.set(.ESCAPE, 0, .SELECT_FIRST_REGION) 53 | map.set(.TAB, .SHIFT, .INSERT_TAB_BACKWARD) 54 | map.set(.TAB, 0, .INSERT_TAB_FORWARD) 55 | 56 | # The terminal uses Command+SPACEBAR instead of shift to toggle selection 57 | if BUILD == .TERMINAL { 58 | map.set(.SPACEBAR, base, .SELECT_TOGGLE) 59 | } 60 | 61 | # These are from OS X spellcheck 62 | map.set(.SEMICOLON, base, .SELECT_NEXT_DIAGNOSTIC) 63 | map.set(.SEMICOLON, base | .SHIFT, .SELECT_PREVIOUS_DIAGNOSTIC) 64 | 65 | # These shortcuts are from Eclipse 66 | map.set(.COMMA, .ALT, .SELECT_PREVIOUS_DIAGNOSTIC) 67 | map.set(.PERIOD, .ALT, .SELECT_NEXT_DIAGNOSTIC) 68 | 69 | _setMoveAndSelectShortcuts(map, .ARROW_LEFT, .ARROW_RIGHT, 0, .MOVE_LEFT_CHARACTER, .MOVE_RIGHT_CHARACTER, .SELECT_LEFT_CHARACTER, .SELECT_RIGHT_CHARACTER) 70 | _setMoveAndSelectShortcuts(map, .ARROW_LEFT, .ARROW_RIGHT, isOSX ? .ALT : .CONTROL, .MOVE_LEFT_WORD, .MOVE_RIGHT_WORD, .SELECT_LEFT_WORD, .SELECT_RIGHT_WORD) 71 | _setMoveAndSelectShortcuts(map, .PAGE_UP, .PAGE_DOWN, 0, .MOVE_UP_PAGE, .MOVE_DOWN_PAGE, .SELECT_UP_PAGE, .SELECT_DOWN_PAGE) 72 | _setMoveAndSelectShortcuts(map, .ARROW_UP, .ARROW_DOWN, 0, .MOVE_UP_LINE, .MOVE_DOWN_LINE, .SELECT_UP_LINE, .SELECT_DOWN_LINE) 73 | _setDoubleShortcuts(map, .BACKSPACE, .DELETE, 0, .DELETE_LEFT_CHARACTER, .DELETE_RIGHT_CHARACTER) 74 | _setDoubleShortcuts(map, .BACKSPACE, .DELETE, .SHIFT, .DELETE_LEFT_CHARACTER, .DELETE_RIGHT_CHARACTER) 75 | _setDoubleShortcuts(map, .BACKSPACE, .DELETE, isOSX ? .ALT : .CONTROL, .DELETE_LEFT_WORD, .DELETE_RIGHT_WORD) 76 | _setDoubleShortcuts(map, .ARROW_UP, .ARROW_DOWN, base | .ALT, .INSERT_CURSOR_ABOVE, .INSERT_CURSOR_BELOW) 77 | 78 | if isOSX { 79 | _setDoubleShortcuts(map, .HOME, .END, 0, .SCROLL_UP_DOCUMENT, .SCROLL_DOWN_DOCUMENT) 80 | _setMoveAndSelectShortcuts(map, .ARROW_LEFT, .ARROW_RIGHT, .META, .MOVE_LEFT_LINE, .MOVE_RIGHT_LINE, .SELECT_LEFT_LINE, .SELECT_RIGHT_LINE) 81 | _setMoveAndSelectShortcuts(map, .ARROW_UP, .ARROW_DOWN, .ALT, .MOVE_LEFT_LINE, .MOVE_RIGHT_LINE, .SELECT_LEFT_LINE, .SELECT_RIGHT_LINE) 82 | _setMoveAndSelectShortcuts(map, .ARROW_UP, .ARROW_DOWN, .META, .MOVE_UP_DOCUMENT, .MOVE_DOWN_DOCUMENT, .SELECT_UP_DOCUMENT, .SELECT_DOWN_DOCUMENT) 83 | _setDoubleShortcuts(map, .BACKSPACE, .DELETE, .META, .DELETE_LEFT_LINE, .DELETE_RIGHT_LINE) 84 | 85 | # Emacs shortcuts (these also work in all OS X text fields) 86 | map.set(.LETTER_K, .CONTROL, .DELETE_RIGHT_LINE) 87 | map.set(.LETTER_H, .CONTROL, .DELETE_LEFT_CHARACTER) 88 | map.set(.LETTER_D, .CONTROL, .DELETE_RIGHT_CHARACTER) 89 | _setMoveAndSelectShortcuts(map, .LETTER_A, .LETTER_E, .CONTROL, .MOVE_LEFT_LINE, .MOVE_RIGHT_LINE, .SELECT_LEFT_LINE, .SELECT_RIGHT_LINE) 90 | _setMoveAndSelectShortcuts(map, .LETTER_P, .LETTER_N, .CONTROL, .MOVE_UP_LINE, .MOVE_DOWN_LINE, .SELECT_UP_LINE, .SELECT_DOWN_LINE) 91 | } 92 | 93 | else { 94 | _setMoveAndSelectShortcuts(map, .HOME, .END, 0, .MOVE_LEFT_LINE, .MOVE_RIGHT_LINE, .SELECT_LEFT_LINE, .SELECT_RIGHT_LINE) 95 | _setMoveAndSelectShortcuts(map, .HOME, .END, .CONTROL, .MOVE_UP_DOCUMENT, .MOVE_DOWN_DOCUMENT, .SELECT_UP_DOCUMENT, .SELECT_DOWN_DOCUMENT) 96 | _setDoubleShortcuts(map, .ARROW_UP, .ARROW_DOWN, .CONTROL, .SCROLL_UP_LINE, .SCROLL_DOWN_LINE) 97 | } 98 | } 99 | 100 | def _setDoubleShortcuts(map ShortcutMap, a UI.Key, b UI.Key, modifiers UI.Modifiers, actionA Action, actionB Action) { 101 | map.set(a, modifiers, actionA) 102 | map.set(b, modifiers, actionB) 103 | } 104 | 105 | def _setMoveAndSelectShortcuts(map ShortcutMap, a UI.Key, b UI.Key, modifiers UI.Modifiers, moveA Action, moveB Action, selectA Action, selectB Action) { 106 | _setDoubleShortcuts(map, a, b, modifiers, moveA, moveB) 107 | _setDoubleShortcuts(map, a, b, modifiers | .SHIFT, selectA, selectB) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/editor/windowcontroller.sk: -------------------------------------------------------------------------------- 1 | namespace Editor { 2 | interface WindowObserver { 3 | def handleAction(action Action) 4 | def handleFrame 5 | } 6 | 7 | class WindowController :: UI.WindowDelegate { 8 | const _window UI.Window 9 | const _observers List = [] 10 | 11 | def new(platform UI.Platform) { 12 | var window = platform.createWindow 13 | window.setTitle("Untitled - Sky Text Editor") 14 | window.setTheme(.XCODE) 15 | window.setFont(.CODE_FONT, MONOSPACE_FONTS, 12, 16, 0) 16 | window.setFont(.MARGIN_FONT, MONOSPACE_FONTS, 10, 14, 0) 17 | window.setFont(.UI_FONT, SANS_SERIF_FONTS, 12, 16, 0) 18 | _window = window 19 | 20 | var model = Model.new("Untitled", platform, window.renderer.fontInstance(.CODE_FONT), 2) 21 | model.setLexer(.SKEW) 22 | 23 | var view = View.new(self, model) 24 | if BUILD != .TERMINAL { 25 | view.changePadding(5, 5, 5, 5) 26 | view.changeMarginPadding(15, 5) 27 | } else { 28 | view.setScrollbarThickness(1) 29 | view.changePadding(0, 0, 1, 0) # Leave one character of padding on the right so the caret can be seen at the end of the line 30 | view.changeMarginPadding(1, 1) 31 | } 32 | view.appendTo(window.root) 33 | 34 | window.setDelegate(self) 35 | window.focusView(view) 36 | } 37 | 38 | def window UI.Window { 39 | return _window 40 | } 41 | 42 | def addObserver(observer WindowObserver) { 43 | _observers.appendOne(observer) 44 | } 45 | 46 | def removeObserver(observer WindowObserver) { 47 | _observers.removeOne(observer) 48 | } 49 | 50 | def triggerFrame { 51 | for observer in _observers { 52 | observer.handleFrame 53 | } 54 | } 55 | 56 | def triggerAction(action Action) { 57 | for observer in _observers { 58 | observer.handleAction(action) 59 | } 60 | } 61 | } 62 | 63 | namespace WindowController { 64 | const MONOSPACE_FONTS = [ 65 | "Monaco", 66 | "Menlo", 67 | "Consolas", 68 | "Courier New", 69 | "monospace", 70 | ] 71 | 72 | const SANS_SERIF_FONTS = [ 73 | "San Francisco", 74 | "Lucida Grande", 75 | "Segoe UI", 76 | "Arial", 77 | "sans-serif", 78 | ] 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/graphics/context.sk: -------------------------------------------------------------------------------- 1 | namespace Graphics { 2 | enum BlendOperation { 3 | ZERO 4 | ONE 5 | 6 | SOURCE_COLOR 7 | TARGET_COLOR 8 | INVERSE_SOURCE_COLOR 9 | INVERSE_TARGET_COLOR 10 | 11 | SOURCE_ALPHA 12 | TARGET_ALPHA 13 | INVERSE_SOURCE_ALPHA 14 | INVERSE_TARGET_ALPHA 15 | 16 | CONSTANT 17 | INVERSE_CONSTANT 18 | } 19 | 20 | enum Primitive { 21 | TRIANGLES 22 | TRIANGLE_STRIP 23 | } 24 | 25 | interface Context { 26 | def addContextResetHandler(callback fn()) 27 | def clear(color RGBA) 28 | def createMaterial(format VertexFormat, vertexSource string, fragmentSource string) Material 29 | def createRenderTarget(texture Texture) RenderTarget 30 | def createTexture(format TextureFormat, width int, height int) Texture { return createTexture(format, width, height, null) } 31 | def createTexture(format TextureFormat, width int, height int, pixels FixedArray) Texture 32 | def draw(primitive Primitive, material Material, vertices FixedArray) 33 | def height int 34 | def removeContextResetHandler(callback fn()) 35 | def resize(width int, height int) 36 | def setRenderTarget(renderTarget RenderTarget) 37 | def setViewport(x int, y int, width int, height int) 38 | def width int 39 | 40 | def setBlendState(source BlendOperation, target BlendOperation) 41 | def setCopyBlendState { setBlendState(.ONE, .ZERO) } 42 | def setAddBlendState { setBlendState(.ONE, .ONE) } 43 | def setPremultipliedBlendState { setBlendState(.ONE, .INVERSE_SOURCE_ALPHA) } 44 | def setUnpremultipliedBlendState { setBlendState(.SOURCE_ALPHA, .INVERSE_SOURCE_ALPHA) } 45 | } 46 | 47 | interface Material { 48 | def context Context 49 | def format VertexFormat 50 | def setUniformFloat(name string, x double) 51 | def setUniformInt(name string, x int) 52 | def setUniformVec2(name string, x double, y double) 53 | def setUniformVec3(name string, x double, y double, z double) 54 | def setUniformVec4(name string, x double, y double, z double, w double) 55 | def setUniformVec4(name string, c RGBA) { setUniformVec4(name, c.red / 255.0, c.green / 255.0, c.blue / 255.0, c.alpha / 255.0) } 56 | def setUniformSampler(name string, texture Texture, index int) 57 | } 58 | 59 | enum AttributeType { 60 | FLOAT 61 | BYTE 62 | 63 | def byteLength int { 64 | return self == FLOAT ? 4 : 1 65 | } 66 | } 67 | 68 | class Attribute { 69 | const name string 70 | const type AttributeType 71 | const count int 72 | const byteOffset int 73 | } 74 | 75 | class VertexFormat { 76 | var _attributes List = [] 77 | var _stride = 0 78 | 79 | def attributes List { 80 | return _attributes 81 | } 82 | 83 | def stride int { 84 | return _stride 85 | } 86 | 87 | def add(name string, type AttributeType, count int) VertexFormat { 88 | _attributes.append(Attribute.new(name, type, count, _stride)) 89 | _stride += count * type.byteLength 90 | return self 91 | } 92 | } 93 | 94 | namespace VertexFormat { 95 | const POSITION_F2 = VertexFormat.new 96 | .add(GLSLX_NAME_POSITION2, .FLOAT, 2) 97 | 98 | const POSITION_F4_COLOR_U4 = VertexFormat.new 99 | .add(GLSLX_NAME_POSITION4, .FLOAT, 4) 100 | .add(GLSLX_NAME_COLOR4, .BYTE, 4) 101 | 102 | const POSITION_F4_COLOR_U4_COORD_U4 = VertexFormat.new 103 | .add(GLSLX_NAME_POSITION4, .FLOAT, 4) 104 | .add(GLSLX_NAME_COLOR4, .BYTE, 4) 105 | .add(GLSLX_NAME_COORD4, .BYTE, 4) 106 | } 107 | 108 | enum PixelFilter { 109 | NEAREST 110 | LINEAR 111 | } 112 | 113 | enum PixelWrap { 114 | REPEAT 115 | CLAMP 116 | } 117 | 118 | class TextureFormat { 119 | const minFilter PixelFilter 120 | const magFilter PixelFilter 121 | const wrap PixelWrap 122 | } 123 | 124 | namespace TextureFormat { 125 | const LINEAR_CLAMP = new(.LINEAR, .LINEAR, .CLAMP) 126 | const LINEAR_MIN_NEAREST_MAG_CLAMP = new(.LINEAR, .NEAREST, .CLAMP) 127 | const NEAREST_CLAMP = new(.NEAREST, .NEAREST, .CLAMP) 128 | } 129 | 130 | interface Texture { 131 | def context Context 132 | def format TextureFormat 133 | def height int 134 | def resize(width int, height int) { resize(width, height, null) } 135 | def resize(width int, height int, pixels FixedArray) 136 | def upload(sourcePixels FixedArray, targetX int, targetY int, sourceWidth int, sourceHeight int) 137 | def width int 138 | } 139 | 140 | interface RenderTarget { 141 | def context Context 142 | def texture Texture 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/graphics/dropshadow.sk: -------------------------------------------------------------------------------- 1 | namespace Graphics { 2 | class DropShadow { 3 | var _width = 0.0 4 | var _height = 0.0 5 | var _context Context = null 6 | var _material Material = null 7 | var _vertices = FixedArray.new(4 * 2 * 4) 8 | 9 | def new(context Context) { 10 | _context = context 11 | _material = context.createMaterial(.POSITION_F2, GLSLX_SOURCE_DROP_SHADOW_VERTEX, GLSLX_SOURCE_DROP_SHADOW_FRAGMENT) 12 | } 13 | 14 | def resize(width double, height double) { 15 | _width = width 16 | _height = height 17 | _material.setUniformVec2(GLSLX_NAME_SCALE2, 2.0 / _width, -2.0 / _height) 18 | } 19 | 20 | def render( 21 | boxX double, boxY double, boxWidth double, boxHeight double, 22 | clipX double, clipY double, clipWidth double, clipHeight double, 23 | shadowAlpha double, blurSigma double) { 24 | 25 | if clipX >= _width || clipY >= _height || clipX + clipWidth <= 0 || clipY + clipHeight <= 0 { 26 | return 27 | } 28 | 29 | _vertices.setFloat(0, clipX) 30 | _vertices.setFloat(4, clipY) 31 | 32 | _vertices.setFloat(8, clipX + clipWidth) 33 | _vertices.setFloat(12, clipY) 34 | 35 | _vertices.setFloat(16, clipX) 36 | _vertices.setFloat(20, clipY + clipHeight) 37 | 38 | _vertices.setFloat(24, clipX + clipWidth) 39 | _vertices.setFloat(28, clipY + clipHeight) 40 | 41 | _material.setUniformVec4(GLSLX_NAME_DROP_SHADOW_BOX, boxX, boxY, boxX + boxWidth, boxY + boxHeight) 42 | _material.setUniformVec2(GLSLX_NAME_DROP_SHADOW_OPTIONS, shadowAlpha, blurSigma) 43 | 44 | _context.setPremultipliedBlendState 45 | _context.draw(.TRIANGLE_STRIP, _material, _vertices) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/graphics/glyphbatch.sk: -------------------------------------------------------------------------------- 1 | namespace Graphics { 2 | class Glyph { 3 | const codePoint int 4 | const mask Mask 5 | const originX int 6 | const originY int 7 | const maskScale double 8 | const advanceWidth double 9 | } 10 | 11 | class GlyphMetadata { 12 | const glyph Glyph 13 | var coords TextureCoords 14 | } 15 | 16 | # A high-level wrapper for appending glyphs into a single texture. Owns 17 | # and manages the texture, including growing it as needed. Can pack glyphs 18 | # from multiple fonts into the same texture. 19 | class GlyphBatch { 20 | const _context Context 21 | const _material Material 22 | var _texture Texture = null 23 | var _packer MaskPacker = null 24 | var _buffer = GrowableArray.new 25 | var _metadata IntMap = null 26 | var _packerWidth = 512 27 | var _packerHeight = 512 28 | var _width = 0.0 29 | var _height = 0.0 30 | var _pixelScale = 0.0 31 | var _backgroundLuminanceByte = 0 32 | 33 | def new(platform UI.Platform, context Context) { 34 | _context = context 35 | _material = context.createMaterial(.POSITION_F4_COLOR_U4_COORD_U4, GLSLX_SOURCE_GLYPH_BATCH_VERTEX, GLSLX_SOURCE_GLYPH_BATCH_FRAGMENT) 36 | _material.setUniformInt(GLSLX_NAME_APPLY_GAMMA_HACK, (TARGET == .JAVASCRIPT) as int) 37 | } 38 | 39 | def resize(width double, height double, pixelScale double) { 40 | _width = width 41 | _height = height 42 | _material.setUniformVec2(GLSLX_NAME_SCALE2, 2 / width, -2 / height) 43 | 44 | # Clear the cache on a DPI change 45 | if pixelScale != _pixelScale { 46 | _pixelScale = pixelScale 47 | _metadata = {} 48 | _packer = MaskPacker.new(_packerWidth, _packerHeight, 1) 49 | } 50 | } 51 | 52 | def setBackgroundColor(color Graphics.RGBA) { 53 | _backgroundLuminanceByte = color.luminance 54 | } 55 | 56 | def appendGlyph(fontInstance UI.FontInstance, codePoint int, x double, y double, color RGBA) double { 57 | var key = codePoint | fontInstance.font << 21 # All unicode code points are < 0x110000 58 | var metadata = _metadata.get(key, null) 59 | 60 | if metadata == null { 61 | # Culling before rendering 62 | var advanceWidth = fontInstance.advanceWidth(codePoint) 63 | if x >= _width || y >= _height || x + advanceWidth <= 0 || y + fontInstance.size <= 0 { 64 | return advanceWidth 65 | } 66 | 67 | # Render uncached glyphs once and pack them together 68 | var glyph = fontInstance.renderGlyph(codePoint) 69 | var coords = _packer.append(glyph.mask) 70 | metadata = GlyphMetadata.new(glyph, coords) 71 | _metadata[key] = metadata 72 | 73 | # Repack using a larger size if we ran out of space 74 | if coords == null { 75 | flush 76 | _repackAllGlyphs 77 | coords = metadata.coords 78 | } 79 | } 80 | 81 | # Culling 82 | else if x >= _width || y >= _height || x + metadata.glyph.advanceWidth <= 0 || y + fontInstance.size <= 0 { 83 | return metadata.glyph.advanceWidth 84 | } 85 | 86 | # Append a single textured quad 87 | var glyph = metadata.glyph 88 | var coords = metadata.coords 89 | var buffer = _buffer 90 | var backgroundLuminanceByte = _backgroundLuminanceByte 91 | var mask = glyph.mask 92 | var maskScale = glyph.maskScale 93 | var shift = x - Math.floor(x) 94 | var shiftByte = (shift * 255) as int 95 | var minX = x - glyph.originX * maskScale - shift 96 | var minY = y - glyph.originY * maskScale 97 | var maxX = minX + mask.width * maskScale 98 | var maxY = minY + mask.height * maskScale 99 | var minU = coords.minU 100 | var minV = coords.minV 101 | var maxU = coords.maxU 102 | var maxV = coords.maxV 103 | 104 | # Triangle 1 105 | buffer.appendVertex(minX, minY, minU, minV, color, shiftByte, backgroundLuminanceByte, 0, 0) 106 | buffer.appendVertex(maxX, minY, maxU, minV, color, shiftByte, backgroundLuminanceByte, 0, 0) 107 | buffer.appendVertex(maxX, maxY, maxU, maxV, color, shiftByte, backgroundLuminanceByte, 0, 0) 108 | 109 | # Triangle 2 110 | buffer.appendVertex(minX, minY, minU, minV, color, shiftByte, backgroundLuminanceByte, 0, 0) 111 | buffer.appendVertex(maxX, maxY, maxU, maxV, color, shiftByte, backgroundLuminanceByte, 0, 0) 112 | buffer.appendVertex(minX, maxY, minU, maxV, color, shiftByte, backgroundLuminanceByte, 0, 0) 113 | 114 | return glyph.advanceWidth 115 | } 116 | 117 | def flush { 118 | if !_buffer.isEmpty { 119 | assert(_packerWidth > 0 && _packerHeight > 0) 120 | 121 | # Create or resize the texture if needed 122 | if _texture == null { 123 | _texture = _context.createTexture(.NEAREST_CLAMP, _packerWidth, _packerHeight) 124 | _material.setUniformSampler(GLSLX_NAME_TEXTURE, _texture, 0) 125 | } else if _texture.width != _packerWidth || _texture.height != _packerHeight { 126 | _texture.resize(_packerWidth, _packerHeight) 127 | } 128 | 129 | # Only upload the texture data in the dirty rectangle if any 130 | var bounds = _packer.stealDirtyBounds 131 | if bounds != null { 132 | var slice = Mask.new(bounds.width, bounds.height) 133 | slice.copyFrom(0, 0, _packer.mask, bounds.minX, bounds.minY, bounds.maxX, bounds.maxY) 134 | _texture.upload(slice.pixels, bounds.minX, bounds.minY, bounds.width, bounds.height) 135 | Log.info("upload glyph mask region with offset (\(bounds.minX), \(bounds.minY)) and " + 136 | "size \(bounds.width)x\(bounds.height) to texture with size \(_packerWidth)x\(_packerHeight)") 137 | } 138 | 139 | _context.setPremultipliedBlendState 140 | _context.draw(.TRIANGLES, _material, _buffer.fixedArray) 141 | _buffer.clear 142 | } 143 | } 144 | 145 | def _repackAllGlyphs { 146 | var metadataValues = _metadata.values 147 | var failed = true 148 | 149 | Log.warning("ran out of glyph mask space at \(_packerWidth)x\(_packerHeight)") 150 | 151 | # Sort glyphs by height to pack them tighter 152 | metadataValues.sort(SORT_BY_HEIGHT) 153 | 154 | while failed { 155 | failed = false 156 | 157 | # Try to maintain a square-ish texture for good spatial cache locality 158 | if _packerWidth > _packerHeight { 159 | _packerHeight *= 2 160 | } else { 161 | _packerWidth *= 2 162 | } 163 | 164 | Log.info("expanding glyph mask space to \(_packerWidth)x\(_packerHeight)") 165 | _packer = MaskPacker.new(_packerWidth, _packerHeight, 1) 166 | 167 | # Re-append each glyph mask, stopping when one doesn't fit. It is 168 | # likely impossible that a glyph wouldn't fit in a glyph cache twice 169 | # as big as the previous one but the iteration loop is here for 170 | # completeness. Don't worry about making a texture that's too big 171 | # since most cards support at least 4k by 4k and that is a LOT of 172 | # glyphs, especially for a text editor that only uses a few fonts. 173 | for i = 0; !failed && i < metadataValues.count; i++ { 174 | var metadata = metadataValues[i] 175 | metadata.coords = _packer.append(metadata.glyph.mask) 176 | failed = metadata.coords == null 177 | } 178 | } 179 | } 180 | } 181 | 182 | namespace GlyphBatch { 183 | const SORT_BY_HEIGHT = (a GlyphMetadata, b GlyphMetadata) => a.glyph.mask.height <=> b.glyph.mask.height 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/graphics/maskpacker.sk: -------------------------------------------------------------------------------- 1 | namespace Graphics { 2 | class Mask { 3 | const width = 0 4 | const height = 0 5 | const pixels FixedArray = null 6 | 7 | def new(width int, height int) { 8 | self.width = width 9 | self.height = height 10 | pixels = FixedArray.new(width * height * 4) 11 | } 12 | 13 | def copyFrom(targetX int, targetY int, source Mask, sourceMinX int, sourceMinY int, sourceMaxX int, sourceMaxY int) { 14 | var sourceData = source.pixels 15 | var targetData = pixels 16 | var sourceStride = source.width 17 | var targetStride = width 18 | var runByteCount = (sourceMaxX - sourceMinX) * 4 19 | 20 | assert(0 <= sourceMinX && sourceMinX <= sourceMaxX && sourceMaxX <= source.width) 21 | assert(0 <= sourceMinY && sourceMinY <= sourceMaxY && sourceMaxY <= source.height) 22 | assert(0 <= targetX && targetX + sourceMaxX - sourceMinX <= width) 23 | assert(0 <= targetY && targetY + sourceMaxY - sourceMinY <= height) 24 | 25 | for y in sourceMinY..sourceMaxY { 26 | targetData.setRange((targetX + (targetY - sourceMinY + y) * targetStride) * 4, 27 | sourceData.getRange((sourceMinX + y * sourceStride) * 4, runByteCount)) 28 | } 29 | } 30 | } 31 | 32 | class TextureCoords { 33 | const minU double 34 | const minV double 35 | const maxU double 36 | const maxV double 37 | } 38 | 39 | class DirtyBounds { 40 | var minX int 41 | var minY int 42 | var maxX int 43 | var maxY int 44 | 45 | def width int { 46 | return maxX - minX 47 | } 48 | 49 | def height int { 50 | return maxY - minY 51 | } 52 | 53 | def extend(newMinX int, newMinY int, newMaxX int, newMaxY int) { 54 | minX = Math.min(minX, newMinX) 55 | minY = Math.min(minY, newMinY) 56 | maxX = Math.max(maxX, newMaxX) 57 | maxY = Math.max(maxY, newMaxY) 58 | } 59 | } 60 | 61 | class MaskPacker { 62 | var _x = 0 63 | var _y = 0 64 | var _height = 0 65 | var _dirtyBounds DirtyBounds = null 66 | const _padding = 0 67 | const _mask Mask = null 68 | 69 | def new(width int, height int, padding int) { 70 | _padding = padding 71 | _mask = Mask.new(width, height) 72 | } 73 | 74 | def mask Mask { 75 | return _mask 76 | } 77 | 78 | def stealDirtyBounds DirtyBounds { 79 | var dirtyBounds = _dirtyBounds 80 | _dirtyBounds = null 81 | return dirtyBounds 82 | } 83 | 84 | # Attempt to pack this mask in with all of the others. Masks are packed in 85 | # rows from left to right. More advanced packing algorithms are avoided 86 | # for speed and simplicity. Plus, the gains of complicated algorithms are 87 | # apparently very small (~5%) when the masks are around the same size, 88 | # which is the case for font glyphs. 89 | def append(mask Mask) TextureCoords { 90 | var padding = _padding 91 | var maskWidth = mask.width 92 | var maskHeight = mask.height 93 | var width = maskWidth + padding * 2 94 | var height = maskHeight + padding * 2 95 | var totalWidth = _mask.width 96 | var totalHeight = _mask.height 97 | var overflow = _x + width > totalWidth 98 | 99 | # Return null to indicate that there's no more room. It's up to the 100 | # caller to handle this case. One way to handle this is to make a new, 101 | # larger packer and re-append all previous masks. 102 | if _y + height > totalHeight || overflow && _y + _height + height > totalHeight { 103 | return null 104 | } 105 | 106 | # Wrap to the next line when this mask would extend past the right side 107 | if overflow { 108 | Log.info("line overflow when packing glyph masks, height is now \(_height)") 109 | _x = 0 110 | _y += _height 111 | } 112 | 113 | # Copy the mask data 114 | var paddedMinX = _x 115 | var paddedMinY = _y 116 | var paddedMaxX = paddedMinX + width 117 | var paddedMaxY = paddedMinY + height 118 | var unpaddedMinX = _x + padding 119 | var unpaddedMinY = _y + padding 120 | var unpaddedMaxX = unpaddedMinX + maskWidth 121 | var unpaddedMaxY = unpaddedMinY + maskHeight 122 | var scaleX = 1.0 / totalWidth 123 | var scaleY = 1.0 / totalHeight 124 | _mask.copyFrom(unpaddedMinX, unpaddedMinY, mask, 0, 0, maskWidth, maskHeight) 125 | _x += width 126 | _height = Math.max(_height, height) 127 | 128 | # Extend the upload rectangle with the uploaded region. Use the padded 129 | # bounds so old texture contents will be cleared around the new mask. 130 | if _dirtyBounds == null { 131 | _dirtyBounds = DirtyBounds.new( 132 | paddedMinX, 133 | paddedMinY, 134 | paddedMaxX, 135 | paddedMaxY) 136 | } else { 137 | _dirtyBounds.extend( 138 | paddedMinX, 139 | paddedMinY, 140 | paddedMaxX, 141 | paddedMaxY) 142 | } 143 | 144 | # Compute the corresponding texture coordinates 145 | return TextureCoords.new( 146 | unpaddedMinX * scaleX, 147 | unpaddedMinY * scaleY, 148 | unpaddedMaxX * scaleX, 149 | unpaddedMaxY * scaleY) 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/graphics/rgba.sk: -------------------------------------------------------------------------------- 1 | namespace Graphics { 2 | # Colors are stored as 8-bit RGBA values and will be constant-folded at 3 | # compile time in release mode. They are stored using a wrapped type instead 4 | # of int for type safety and convenience. 5 | type RGBA : int { 6 | def red int { 7 | return (self as int) >> 16 & 255 8 | } 9 | 10 | def green int { 11 | return (self as int) >> 8 & 255 12 | } 13 | 14 | def blue int { 15 | return (self as int) & 255 16 | } 17 | 18 | def alpha int { 19 | return (self as int) >>> 24 20 | } 21 | 22 | def multiplyAlpha(a int) RGBA { 23 | assert(a >= 0 && a <= 0xFF) 24 | return new(red, green, blue, alpha * a / 0xFF) 25 | } 26 | 27 | def premultiplied RGBA { 28 | var a = alpha 29 | return new(red * a / 0xFF, green * a / 0xFF, blue * a / 0xFF, a) 30 | } 31 | 32 | def luminance int { 33 | return (0.299 * red + 0.587 * green + 0.114 * blue) as int 34 | } 35 | 36 | def toString string { 37 | return "RGBA(\(red), \(green), \(blue), \(alpha))" 38 | } 39 | } 40 | 41 | namespace RGBA { 42 | const TRANSPARENT = new(0, 0, 0, 0) 43 | const BLACK = hex(0x000000) 44 | const WHITE = hex(0xFFFFFF) 45 | 46 | const RED = hex(0xFF0000) 47 | const GREEN = hex(0x00FF00) 48 | const BLUE = hex(0x0000FF) 49 | 50 | const CYAN = hex(0x00FFFF) 51 | const MAGENTA = hex(0xFF00FF) 52 | const YELLOW = hex(0xFFFF00) 53 | 54 | def new(r int, g int, b int) RGBA { 55 | return new(r, g, b, 255) 56 | } 57 | 58 | def new(r int, g int, b int, a int) RGBA { 59 | assert(r >= 0 && r <= 0xFF) 60 | assert(g >= 0 && g <= 0xFF) 61 | assert(b >= 0 && b <= 0xFF) 62 | assert(a >= 0 && a <= 0xFF) 63 | return (r << 16 | g << 8 | b | a << 24) as RGBA 64 | } 65 | 66 | def hex(rgb int) RGBA { 67 | return hex(rgb, 255) 68 | } 69 | 70 | def hex(rgb int, alpha int) RGBA { 71 | assert(rgb >= 0 && rgb <= 0xFFFFFF) 72 | assert(alpha >= 0 && alpha <= 0xFF) 73 | return (rgb | alpha << 24) as RGBA 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/graphics/shaders.sk: -------------------------------------------------------------------------------- 1 | const GLSLX_SOURCE_SOLID_BATCH_VERTEX = "precision highp float;uniform vec2 e;attribute vec4 i,h;varying vec4 f,c;void main(){c=h,f=i,gl_Position=vec4(h.xy*e-sign(e),0,1);}" 2 | const GLSLX_SOURCE_SOLID_BATCH_FRAGMENT = "precision highp float;varying vec4 f,c;void main(){gl_FragColor=f*min(1.,min(c.z,c.w));}" 3 | const GLSLX_SOURCE_GLYPH_BATCH_VERTEX = "precision highp float;uniform vec2 e;attribute vec4 i,k,h;varying vec4 f,c;void main(){c=vec4(h.zw,k.x*3.,k.y),f=i,gl_Position=vec4(h.xy*e-sign(e),0,1);}" 4 | const GLSLX_SOURCE_GLYPH_BATCH_FRAGMENT = "precision highp float;uniform sampler2D o;uniform bool p;varying vec4 f,c;void main(){vec4 d=texture2D(o,c.xy);float a=c.w,g=dot(f.rgb,vec3(.299,.587,.114)),b=c.z<1.?d.x:c.z<2.?d.y:d.z;p?b+=((sqrt(mix(a*a,g*g,sqrt(b)))-a)/(g-a)-b)*.5:0.,gl_FragColor=f*b;}" 5 | const GLSLX_SOURCE_DROP_SHADOW_VERTEX = "precision highp float;uniform vec2 e;attribute vec2 l;varying vec2 j;void main(){j=l,gl_Position=vec4(l.xy*e-sign(e),0,1);}" 6 | const GLSLX_SOURCE_DROP_SHADOW_FRAGMENT = "precision highp float;uniform vec2 m;uniform vec4 n;varying vec2 j;vec4 s(vec4 a){vec4 d=sign(a),b=abs(a);a=b*(b*(b*b*.078108+.230389)+.278393)+1.,a*=a;return d-d/(a*a);}float q(vec2 d,vec2 g,vec2 b,float r){vec4 t=vec4(b-d,b-g),a=.5+.5*s(t*(sqrt(.5)/r));return (a.z-a.x)*(a.w-a.y);}float u(){return fract(sin(dot(vec2(12.9898,78.233),gl_FragCoord.xy))*43758.5453);}void main(){float a=m.x*q(n.xy,n.zw,j.xy,m.y);a+=(u()-.5)/128.,gl_FragColor=vec4(0,0,0,a);}" 7 | 8 | const GLSLX_NAME_SCALE2 = "e" 9 | const GLSLX_NAME_POSITION4 = "h" 10 | const GLSLX_NAME_COLOR4 = "i" 11 | const GLSLX_NAME_COORD4 = "k" 12 | const GLSLX_NAME_POSITION2 = "l" 13 | const GLSLX_NAME_DROP_SHADOW_OPTIONS = "m" 14 | const GLSLX_NAME_DROP_SHADOW_BOX = "n" 15 | const GLSLX_NAME_TEXTURE = "o" 16 | const GLSLX_NAME_APPLY_GAMMA_HACK = "p" 17 | -------------------------------------------------------------------------------- /src/graphics/solidbatch.sk: -------------------------------------------------------------------------------- 1 | namespace Graphics { 2 | enum StrokeCap { 3 | CLOSED 4 | OPEN 5 | OPEN_WITHOUT_ENDPOINT_AA 6 | } 7 | 8 | class SolidBatch { 9 | const _inside List = [] 10 | const _material Material = null 11 | const _outside List = [] 12 | const _vertices = GrowableArray.new 13 | var _context Context = null 14 | var _width = 0.0 15 | var _height = 0.0 16 | var _inversePixelScale = 1.0 17 | var _previousColor RGBA = .TRANSPARENT 18 | var _previousU = 0.0 19 | var _previousV = 0.0 20 | var _previousX = 0.0 21 | var _previousY = 0.0 22 | 23 | def new(context Context) { 24 | _context = context 25 | _material = context.createMaterial(.POSITION_F4_COLOR_U4, GLSLX_SOURCE_SOLID_BATCH_VERTEX, GLSLX_SOURCE_SOLID_BATCH_FRAGMENT) 26 | } 27 | 28 | def resize(width double, height double, pixelScale double) { 29 | _width = width 30 | _height = height 31 | _material.setUniformVec2(GLSLX_NAME_SCALE2, 2 / width, -2 / height) 32 | _inversePixelScale = 1 / pixelScale 33 | } 34 | 35 | def flush { 36 | if !_vertices.isEmpty { 37 | _context.setPremultipliedBlendState 38 | _context.draw(.TRIANGLE_STRIP, _material, _vertices.fixedArray) 39 | _vertices.clear 40 | } 41 | } 42 | 43 | def fillRect(x double, y double, width double, height double, color RGBA) { 44 | if x >= _width || y >= _height || x + width <= 0 || y + height <= 0 { 45 | return 46 | } 47 | 48 | var coordinates = _rectCoordinates 49 | coordinates[0] = x 50 | coordinates[1] = y 51 | coordinates[2] = x + width 52 | coordinates[3] = y 53 | coordinates[4] = x + width 54 | coordinates[5] = y + height 55 | coordinates[6] = x 56 | coordinates[7] = y + height 57 | fillConvexPolygon(coordinates, color) 58 | } 59 | 60 | def fillRoundedRect(x double, y double, width double, height double, color RGBA, radius double) { 61 | if x >= _width || y >= _height || x + width <= 0 || y + height <= 0 { 62 | return 63 | } 64 | 65 | radius = Math.min(radius, width / 2, height / 2) 66 | 67 | var coordinates List = [] 68 | var minX = x + radius 69 | var minY = y + radius 70 | var maxX = x + width - radius 71 | var maxY = y + height - radius 72 | var isClampedX = radius == width / 2 73 | var isClampedY = radius == height / 2 74 | 75 | _appendQuarterTurn(coordinates, minX, minY, radius, -Math.PI, -Math.PI / 2, isClampedY) 76 | _appendQuarterTurn(coordinates, maxX, minY, radius, -Math.PI / 2, 0, isClampedX) 77 | _appendQuarterTurn(coordinates, maxX, maxY, radius, 0, Math.PI / 2, isClampedY) 78 | _appendQuarterTurn(coordinates, minX, maxY, radius, Math.PI / 2, Math.PI, isClampedX) 79 | 80 | fillConvexPolygon(coordinates, color) 81 | } 82 | 83 | def _appendQuarterTurn(coordinates List, centerX double, centerY double, radius double, fromAngle double, toAngle double, skipStartPoint bool) { 84 | var n = 1 + Math.ceil(radius / (_inversePixelScale * Math.PI)) as int 85 | for i = skipStartPoint as int; i <= n; i++ { 86 | var angle = fromAngle + (toAngle - fromAngle) * i / n 87 | coordinates.append(centerX + Math.cos(angle) * radius) 88 | coordinates.append(centerY + Math.sin(angle) * radius) 89 | } 90 | } 91 | 92 | def strokeLine(startX double, startY double, endX double, endY double, color RGBA, thickness double) { 93 | _strokeLine(startX, startY, endX, endY, color, thickness, .OPEN) 94 | } 95 | 96 | def strokeLineWithoutEndpointAA(startX double, startY double, endX double, endY double, color RGBA, thickness double) { 97 | _strokeLine(startX, startY, endX, endY, color, thickness, .OPEN_WITHOUT_ENDPOINT_AA) 98 | } 99 | 100 | def _strokeLine(startX double, startY double, endX double, endY double, color RGBA, thickness double, stroke StrokeCap) { 101 | assert(stroke != .CLOSED) 102 | var coordinates = _lineCoordinates 103 | coordinates[0] = startX 104 | coordinates[1] = startY 105 | coordinates[2] = endX 106 | coordinates[3] = endY 107 | strokeNonOverlappingPolyline(coordinates, color, thickness, stroke) 108 | } 109 | 110 | def fillConvexPolygon(coordinates List, color RGBA) { 111 | var inside = _inside 112 | var outside = _outside 113 | 114 | var n = coordinates.count 115 | assert(n % 2 == 0) 116 | 117 | if n < 6 { 118 | return 119 | } 120 | 121 | # Reuse existing buffers to avoid extra allocations 122 | while inside.count < n { 123 | inside.append(0) 124 | inside.append(0) 125 | outside.append(0) 126 | outside.append(0) 127 | } 128 | 129 | # Compute both rings of points 130 | for i = 0; i < n; i += 2 { 131 | var v0x = coordinates[(i + n - 2) % n] 132 | var v0y = coordinates[(i + n - 1) % n] 133 | var v1x = coordinates[i] 134 | var v1y = coordinates[i + 1] 135 | var v2x = coordinates[(i + 2) % n] 136 | var v2y = coordinates[(i + 3) % n] 137 | var n01x = v0y - v1y 138 | var n01y = v1x - v0x 139 | var n01 = _length(n01x, n01y) 140 | var n12x = v1y - v2y 141 | var n12y = v2x - v1x 142 | var n12 = _length(n12x, n12y) 143 | var n012x = n01x / n01 + n12x / n12 144 | var n012y = n01y / n01 + n12y / n12 145 | var scale = 0.5 * _inversePixelScale * n01 / (n01x * n012x + n01y * n012y) 146 | var dx = n012x * scale 147 | var dy = n012y * scale 148 | outside[i] = v1x - dx 149 | outside[i + 1] = v1y - dy 150 | inside[i] = v1x + dx 151 | inside[i + 1] = v1y + dy 152 | } 153 | 154 | # Fill the interior with a triangle strip 155 | for i = 0, j = n - 2; i <= j; i += 2, j -= 2 { 156 | var vix = inside[i] 157 | var viy = inside[i + 1] 158 | var vjx = inside[j] 159 | var vjy = inside[j + 1] 160 | _appendVertex(vix, viy, 1, 1, color) 161 | if i == 0 { _appendPreviousVertex } 162 | if i < j { _appendVertex(vjx, vjy, 1, 1, color) } 163 | } 164 | _appendPreviousVertex 165 | 166 | # Outline the edge with anti-aliasing 167 | for i = 0; i <= n; i += 2 { 168 | var j = i == n ? 0 : i 169 | _appendVertex(outside[j], outside[j + 1], 0, 0, color) 170 | if i == 0 { _appendPreviousVertex } 171 | _appendVertex(inside[j], inside[j + 1], 1, 1, color) 172 | } 173 | _appendPreviousVertex 174 | } 175 | 176 | def strokeNonOverlappingPolyline(coordinates List, color RGBA, thickness double, stroke StrokeCap) { 177 | # Need to draw the line wider by one pixel for anti-aliasing 178 | var aa = (thickness + _inversePixelScale) / _inversePixelScale 179 | var halfWidth = (thickness + _inversePixelScale) / 2 180 | var n = coordinates.count 181 | 182 | assert(n % 2 == 0) 183 | if n < 4 { 184 | return 185 | } 186 | 187 | # Emit the start cap 188 | if stroke != .CLOSED { 189 | var v0x = coordinates[0] 190 | var v0y = coordinates[1] 191 | var v1x = coordinates[2] 192 | var v1y = coordinates[3] 193 | var dx = v1x - v0x 194 | var dy = v1y - v0y 195 | var d = _length(dx, dy) 196 | var u = 0.5 * _inversePixelScale / d 197 | var ux = dx * u 198 | var uy = dy * u 199 | var v = halfWidth / d 200 | var vx = -dy * v 201 | var vy = dx * v 202 | if stroke == .OPEN_WITHOUT_ENDPOINT_AA { 203 | _appendVertex(v0x - vx, v0y - vy, 0, aa, color) 204 | _appendPreviousVertex 205 | _appendVertex(v0x + vx, v0y + vy, aa, 0, color) 206 | } else { 207 | _appendVertex(v0x, v0y, aa / 2, aa / 2, color) 208 | _appendPreviousVertex 209 | _appendVertex(v0x + ux + vx, v0y + uy + vy, 0, aa, color) 210 | _appendVertex(v0x - ux + vx, v0y - uy + vy, 0, aa, color) 211 | _appendVertex(v0x - ux - vx, v0y - uy - vy, 0, aa, color) 212 | _appendVertex(v0x, v0y, aa / 2, aa / 2, color) 213 | _appendVertex(v0x + ux - vx, v0y + uy - vy, 0, aa, color) 214 | _appendVertex(v0x + ux + vx, v0y + uy + vy, aa, 0, color) 215 | } 216 | } 217 | 218 | # Emit the joins between segments 219 | var minJoin = stroke == .CLOSED ? 0 : 2 220 | var maxJoin = stroke == .CLOSED ? n + 2 : n - 2 221 | for i = minJoin; i < maxJoin; i += 2 { 222 | var v0x = coordinates[(i + n - 2) % n] 223 | var v0y = coordinates[(i + n - 1) % n] 224 | var v1x = coordinates[i % n] 225 | var v1y = coordinates[(i + 1) % n] 226 | var v2x = coordinates[(i + 2) % n] 227 | var v2y = coordinates[(i + 3) % n] 228 | var n01x = v0y - v1y 229 | var n01y = v1x - v0x 230 | var n01 = _length(n01x, n01y) 231 | var n12x = v1y - v2y 232 | var n12y = v2x - v1x 233 | var n12 = _length(n12x, n12y) 234 | var n012x = n01x / n01 + n12x / n12 235 | var n012y = n01y / n01 + n12y / n12 236 | var scale = halfWidth * n01 / (n01x * n012x + n01y * n012y) 237 | var dx = n012x * scale 238 | var dy = n012y * scale 239 | _appendVertex(v1x - dx, v1y - dy, 0, aa, color) 240 | if i == 0 { _appendPreviousVertex } # This only happens in the closed loop case 241 | _appendVertex(v1x + dx, v1y + dy, aa, 0, color) 242 | } 243 | 244 | # Emit the end cap 245 | if stroke != .CLOSED { 246 | var v0x = coordinates[n - 4] 247 | var v0y = coordinates[n - 3] 248 | var v1x = coordinates[n - 2] 249 | var v1y = coordinates[n - 1] 250 | var dx = v1x - v0x 251 | var dy = v1y - v0y 252 | var d = _length(dx, dy) 253 | var u = 0.5 * _inversePixelScale / d 254 | var ux = dx * u 255 | var uy = dy * u 256 | var v = halfWidth / d, vx = -dy * v, vy = dx * v 257 | if stroke == .OPEN_WITHOUT_ENDPOINT_AA { 258 | _appendVertex(v1x - vx, v1y - vy, 0, aa, color) 259 | _appendVertex(v1x + vx, v1y + vy, aa, 0, color) 260 | } else { 261 | _appendVertex(v1x - ux - vx, v1y - uy - vy, 0, aa, color) 262 | _appendVertex(v1x - ux + vx, v1y - uy + vy, aa, 0, color) 263 | _appendVertex(v1x, v1y, aa / 2, aa / 2, color) 264 | _appendVertex(v1x + ux + vx, v1y + uy + vy, aa, 0, color) 265 | _appendVertex(v1x + ux - vx, v1y + uy - vy, aa, 0, color) 266 | _appendVertex(v1x - ux - vx, v1y - uy - vy, aa, 0, color) 267 | _appendVertex(v1x, v1y, aa / 2, aa / 2, color) 268 | } 269 | } 270 | _appendPreviousVertex 271 | } 272 | 273 | def _appendVertex(x double, y double, u double, v double, color RGBA) { 274 | _previousX = x 275 | _previousY = y 276 | _previousU = u 277 | _previousV = v 278 | _previousColor = color 279 | _vertices.appendVertex(x, y, u, v, color) 280 | } 281 | 282 | def _appendPreviousVertex { 283 | _appendVertex(_previousX, _previousY, _previousU, _previousV, _previousColor) 284 | } 285 | 286 | def _length(x double, y double) double { 287 | return Math.sqrt(x * x + y * y) 288 | } 289 | } 290 | 291 | namespace SolidBatch { 292 | const _rectCoordinates List = [0, 0, 0, 0, 0, 0, 0, 0] 293 | const _lineCoordinates List = [0, 0, 0, 0] 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/support/build.sk: -------------------------------------------------------------------------------- 1 | enum Build { 2 | NONE 3 | OSX 4 | TERMINAL 5 | WWW 6 | } 7 | 8 | const BUILD Build = .NONE 9 | -------------------------------------------------------------------------------- /src/support/exports.sk: -------------------------------------------------------------------------------- 1 | @export if BUILD == .OSX || BUILD == .TERMINAL { 2 | namespace Editor {} 3 | namespace UI {} 4 | } 5 | 6 | @export if BUILD == .OSX { 7 | namespace Graphics {} 8 | } 9 | 10 | @export if BUILD == .TERMINAL { 11 | # GCC doesn't support the unicode parts of C++11 yet (wtf it's 2016 already) 12 | def codePointsFromString(text string) List { 13 | return text.codePoints 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/support/fixedarray.sk: -------------------------------------------------------------------------------- 1 | @import if TARGET != .JAVASCRIPT 2 | class FixedArray { 3 | def new(byteCount int) 4 | 5 | def byteCount int 6 | def isEmpty bool { return byteCount == 0 } 7 | 8 | def getByte(byteIndex int) int 9 | def setByte(byteIndex int, value int) 10 | 11 | def getFloat(byteIndex int) double 12 | def setFloat(byteIndex int, value double) 13 | 14 | def getRange(byteIndex int, byteCount int) FixedArray 15 | def setRange(byteIndex int, array FixedArray) 16 | 17 | def bytesForJS Uint8Array 18 | } 19 | 20 | if TARGET == .JAVASCRIPT { 21 | class FixedArray { 22 | var _bytes Uint8Array = null 23 | var _floats Float32Array = null 24 | 25 | def new(byteCount int) { 26 | _bytes = Uint8Array.new(byteCount) 27 | _floats = Float32Array.new(_bytes.buffer, 0, byteCount >> 2) 28 | } 29 | 30 | def new(array FixedArray, byteIndex int, byteCount int) { 31 | assert(byteIndex >= 0 && byteCount >= 0 && byteIndex + byteCount <= array.byteCount) 32 | assert(byteCount % 4 == 0) # This is an alignment requirement for Float32Array 33 | _bytes = array._bytes.subarray(byteIndex, byteIndex + byteCount) 34 | _floats = Float32Array.new(array._bytes.buffer, array._bytes.byteOffset + byteIndex, byteCount >> 2) 35 | } 36 | 37 | def byteCount int { 38 | return _bytes.length 39 | } 40 | 41 | def getByte(byteIndex int) int { 42 | assert(byteIndex >= 0 && byteIndex + 1 <= byteCount) 43 | return _bytes[byteIndex] 44 | } 45 | 46 | def setByte(byteIndex int, value int) { 47 | assert(byteIndex >= 0 && byteIndex + 1 <= byteCount) 48 | _bytes[byteIndex] = value 49 | } 50 | 51 | def getFloat(byteIndex int) double { 52 | assert(byteIndex >= 0 && byteIndex + 4 <= byteCount && byteIndex % 4 == 0) 53 | return _floats[byteIndex >> 2] 54 | } 55 | 56 | def setFloat(byteIndex int, value double) { 57 | assert(byteIndex >= 0 && byteIndex + 4 <= byteCount && byteIndex % 4 == 0) 58 | _floats[byteIndex >> 2] = value 59 | } 60 | 61 | def getRange(byteIndex int, byteCount int) FixedArray { 62 | return FixedArray.new(self, byteIndex, byteCount) 63 | } 64 | 65 | def setRange(byteIndex int, array FixedArray) { 66 | assert(byteIndex >= 0 && byteIndex + array.byteCount <= byteCount) 67 | assert(byteIndex % 4 == 0) # This is an alignment requirement for Float32Array 68 | _bytes.set(array._bytes, byteIndex) 69 | } 70 | 71 | def bytesForJS Uint8Array { 72 | return _bytes 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/support/frozenlist.sk: -------------------------------------------------------------------------------- 1 | # An immutable wrapper for a List that is safe to share without fear of 2 | # accidental mutation. Never cast from a FrozenList to a List. 3 | type FrozenList : List { 4 | def thaw List { 5 | return (self as List).clone 6 | } 7 | 8 | def first T { 9 | return (self as List).first 10 | } 11 | 12 | def last T { 13 | return (self as List).last 14 | } 15 | 16 | def isEmpty bool { 17 | return (self as List).isEmpty 18 | } 19 | 20 | def count int { 21 | return (self as List).count 22 | } 23 | 24 | def in(value T) bool { 25 | return value in (self as List) 26 | } 27 | 28 | def [](index int) T { 29 | return (self as List)[index] 30 | } 31 | } 32 | 33 | class List { 34 | def freeze FrozenList { 35 | return clone as FrozenList 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/support/growablearray.sk: -------------------------------------------------------------------------------- 1 | class GrowableArray { 2 | var _array FixedArray = null 3 | var _byteCount = 0 4 | var _byteCapacity = INITIAL_BYTE_CAPACITY 5 | 6 | def new { 7 | _array = FixedArray.new(INITIAL_BYTE_CAPACITY) 8 | } 9 | 10 | def new(array FixedArray) { 11 | _array = array 12 | _byteCount = _byteCapacity = array.byteCount 13 | } 14 | 15 | def clear { 16 | _byteCount = 0 17 | } 18 | 19 | def isEmpty bool { 20 | return _byteCount == 0 21 | } 22 | 23 | def byteCount int { 24 | return _byteCount 25 | } 26 | 27 | def fixedArray FixedArray { 28 | return _array.getRange(0, _byteCount) 29 | } 30 | 31 | def appendRange(array FixedArray) GrowableArray { 32 | _ensureSpace(array.byteCount) 33 | _array.setRange(_byteCount, array) 34 | _byteCount += array.byteCount 35 | return self 36 | } 37 | 38 | def appendByte(value int) GrowableArray { 39 | _ensureSpace(1) 40 | _array.setByte(_byteCount, value) 41 | _byteCount++ 42 | return self 43 | } 44 | 45 | def appendFloat(value double) GrowableArray { 46 | _ensureSpace(4) 47 | _array.setFloat(_byteCount, value) 48 | _byteCount += 4 49 | return self 50 | } 51 | 52 | def appendVertex(x double, y double, u double, v double, color Graphics.RGBA) { 53 | var byteCount = _byteCount 54 | _ensureSpace(4 * 5) 55 | _array.setFloat(byteCount, x) 56 | _array.setFloat(byteCount + 4, y) 57 | _array.setFloat(byteCount + 8, u) 58 | _array.setFloat(byteCount + 12, v) 59 | _array.setByte(byteCount + 16, color.red) 60 | _array.setByte(byteCount + 17, color.green) 61 | _array.setByte(byteCount + 18, color.blue) 62 | _array.setByte(byteCount + 19, color.alpha) 63 | _byteCount = byteCount + 4 * 5 64 | } 65 | 66 | def appendVertex(x double, y double, u double, v double, color Graphics.RGBA, a int, b int, c int, d int) { 67 | var byteCount = _byteCount 68 | _ensureSpace(4 * 6) 69 | _array.setFloat(byteCount, x) 70 | _array.setFloat(byteCount + 4, y) 71 | _array.setFloat(byteCount + 8, u) 72 | _array.setFloat(byteCount + 12, v) 73 | _array.setByte(byteCount + 16, color.red) 74 | _array.setByte(byteCount + 17, color.green) 75 | _array.setByte(byteCount + 18, color.blue) 76 | _array.setByte(byteCount + 19, color.alpha) 77 | _array.setByte(byteCount + 20, a) 78 | _array.setByte(byteCount + 21, b) 79 | _array.setByte(byteCount + 22, c) 80 | _array.setByte(byteCount + 23, d) 81 | _byteCount = byteCount + 4 * 6 82 | } 83 | 84 | def _ensureSpace(space int) { 85 | if _byteCount + space > _byteCapacity { 86 | _byteCapacity *= 2 87 | var array = FixedArray.new(_byteCapacity) 88 | array.setRange(0, _array) 89 | _array = array 90 | } 91 | } 92 | } 93 | 94 | namespace GrowableArray { 95 | const INITIAL_BYTE_CAPACITY = 256 96 | } 97 | -------------------------------------------------------------------------------- /src/support/log.sk: -------------------------------------------------------------------------------- 1 | namespace Log { 2 | const USE_LOG = !RELEASE 3 | 4 | @import if TARGET != .JAVASCRIPT 5 | @skip if !USE_LOG { 6 | def info(text string) 7 | def warning(text string) 8 | def error(text string) 9 | } 10 | } 11 | 12 | if TARGET == .JAVASCRIPT { 13 | namespace Log { 14 | def info(text string) { 15 | var console = Browser.window.console 16 | if console { 17 | console.log(text) 18 | } 19 | } 20 | 21 | def warning(text string) { 22 | var console = Browser.window.console 23 | if console { 24 | console.warn(text) 25 | } 26 | } 27 | 28 | def error(text string) { 29 | var console = Browser.window.console 30 | if console { 31 | console.error(text) 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/support/rect.sk: -------------------------------------------------------------------------------- 1 | class Rect { 2 | const x double 3 | const y double 4 | const width double 5 | const height double 6 | 7 | def left double { 8 | return x 9 | } 10 | 11 | def top double { 12 | return y 13 | } 14 | 15 | def right double { 16 | return x + width 17 | } 18 | 19 | def bottom double { 20 | return y + height 21 | } 22 | 23 | def topLeft Vector { 24 | return Vector.new(top, left) 25 | } 26 | 27 | def topRight Vector { 28 | return Vector.new(top, right) 29 | } 30 | 31 | def bottomLeft Vector { 32 | return Vector.new(bottom, left) 33 | } 34 | 35 | def bottomRight Vector { 36 | return Vector.new(bottom, right) 37 | } 38 | 39 | def size Vector { 40 | return Vector.new(width, height) 41 | } 42 | 43 | def contains(v Vector) bool { 44 | return left <= v.x && v.x < right && top <= v.y && v.y < bottom 45 | } 46 | 47 | def equals(r Rect) bool { 48 | return x == r.x && y == r.y && width == r.width && height == r.height 49 | } 50 | } 51 | 52 | namespace Rect { 53 | const EMPTY = new(0, 0, 0, 0) 54 | } 55 | -------------------------------------------------------------------------------- /src/support/vector.sk: -------------------------------------------------------------------------------- 1 | class Vector { 2 | const x double 3 | const y double 4 | 5 | def - Vector { 6 | return new(-x, -y) 7 | } 8 | 9 | def +(v Vector) Vector { 10 | return new(x + v.x, y + v.y) 11 | } 12 | 13 | def -(v Vector) Vector { 14 | return new(x - v.x, y - v.y) 15 | } 16 | 17 | def equals(v Vector) bool { 18 | return x == v.x && y == v.y 19 | } 20 | } 21 | 22 | namespace Vector { 23 | const ZERO = new(0, 0) 24 | } 25 | -------------------------------------------------------------------------------- /src/syntax/skew.sk: -------------------------------------------------------------------------------- 1 | namespace Syntax.SkewLexer { 2 | const _characterState = Editor.LexerState.new 3 | const _defaultState = Editor.LexerState.new 4 | const _stringState = Editor.LexerState.new 5 | 6 | const _keywords = { 7 | "as": 0, 8 | "break": 0, 9 | "case": 0, 10 | "catch": 0, 11 | "const": 0, 12 | "continue": 0, 13 | "default": 0, 14 | "else": 0, 15 | "finally": 0, 16 | "for": 0, 17 | "if": 0, 18 | "in": 0, 19 | "is": 0, 20 | "return": 0, 21 | "super": 0, 22 | "switch": 0, 23 | "throw": 0, 24 | "try": 0, 25 | "var": 0, 26 | "while": 0, 27 | } 28 | 29 | const _constants = { 30 | "false": 0, 31 | "null": 0, 32 | "self": 0, 33 | "true": 0, 34 | } 35 | 36 | const _definitionKeywords = { 37 | "catch": 0, 38 | "class": 0, 39 | "const": 0, 40 | "def": 0, 41 | "enum": 0, 42 | "flags": 0, 43 | "for": 0, 44 | "interface": 0, 45 | "namespace": 0, 46 | "over": 0, 47 | "type": 0, 48 | "var": 0, 49 | } 50 | 51 | const _types = { 52 | "bool": 0, 53 | "double": 0, 54 | "dynamic": 0, 55 | "int": 0, 56 | "string": 0, 57 | } 58 | 59 | def tokenize(lexer Editor.Lexer) { 60 | var previousIdentifier string = null 61 | 62 | # The first line will have no previous state 63 | if lexer.currentState == null { 64 | assert(lexer.currentIndex == 0) 65 | lexer.transitionToState(_defaultState, 0) 66 | } 67 | 68 | # Scan over the string once 69 | while lexer.hasNext { 70 | var startOfToken = lexer.currentIndex 71 | var state = lexer.currentState 72 | var c = lexer.takeNext 73 | 74 | # Whitespace 75 | if Editor.Lexer.isSpace(c) { 76 | continue 77 | } 78 | 79 | # String and character literal states 80 | else if state == _stringState || state == _characterState { 81 | if c == '\\' && lexer.hasNext { 82 | lexer.next 83 | } else if c == (state == _stringState ? '"' : '\'') { 84 | lexer.addSpan(lexer.startOfState, lexer.currentIndex, state == _stringState ? .FOREGROUND_STRING : .FOREGROUND_NUMBER) 85 | lexer.transitionToState(_defaultState, lexer.currentIndex) 86 | } 87 | } 88 | 89 | # Most of the language is parsed in the default state 90 | else { 91 | assert(state == _defaultState) 92 | 93 | # Identifier or keyword 94 | if Editor.Lexer.isAlpha(c) { 95 | var text = string.fromCodeUnit(c) + lexer.scanAlphaNumericString 96 | var color UI.Color = 97 | text in _keywords ? .FOREGROUND_KEYWORD : 98 | text in _constants ? .FOREGROUND_KEYWORD_CONSTANT : 99 | text.count > 1 && !Editor.Lexer.hasLowerCase(text) ? .FOREGROUND_CONSTANT : 100 | text in _types || Editor.Lexer.isUpperCase(c) || text == "fn" && (!lexer.hasNext || lexer.peekNext == '(') ? .FOREGROUND_TYPE : 101 | c == '_' ? .FOREGROUND_INSTANCE : 102 | .FOREGROUND_DEFAULT 103 | var isAfterDefinitionKeyword = (color == .FOREGROUND_DEFAULT || color == .FOREGROUND_TYPE || color == .FOREGROUND_INSTANCE || color == .FOREGROUND_CONSTANT) && 104 | previousIdentifier != null && previousIdentifier in _definitionKeywords 105 | 106 | # Make definitions stand out 107 | if isAfterDefinitionKeyword { 108 | lexer.changePreviousSpanColor(.FOREGROUND_KEYWORD) 109 | color = .FOREGROUND_DEFINITION 110 | } 111 | 112 | lexer.addSpan(startOfToken, lexer.currentIndex, color) 113 | 114 | previousIdentifier = text 115 | continue 116 | } 117 | 118 | # Annotation 119 | else if c == '@' { 120 | lexer.scanAlphaNumericString 121 | lexer.addSpan(startOfToken, lexer.currentIndex, .FOREGROUND_ANNOTATION) 122 | } 123 | 124 | # Comment 125 | else if c == '#' { 126 | lexer.addSpan(startOfToken, lexer.endOfLine, .FOREGROUND_COMMENT) 127 | return 128 | } 129 | 130 | # Number literal 131 | else if Editor.Lexer.isDigit(c) { 132 | lexer.scanAlphaNumericString 133 | lexer.addSpan(startOfToken, lexer.currentIndex, .FOREGROUND_NUMBER) 134 | } 135 | 136 | # String literal 137 | else if c == '"' { 138 | lexer.transitionToState(_stringState, startOfToken) 139 | } 140 | 141 | # Character literal 142 | else if c == '\'' { 143 | lexer.transitionToState(_characterState, startOfToken) 144 | } 145 | } 146 | 147 | previousIdentifier = null 148 | } 149 | 150 | var endState = lexer.currentState 151 | var color UI.Color = 152 | endState == _stringState ? .FOREGROUND_STRING : 153 | endState == _characterState ? .FOREGROUND_NUMBER : 154 | .FOREGROUND_DEFAULT 155 | 156 | # Add a trailing span if a non-default state continues off the end of the line 157 | if color != .FOREGROUND_DEFAULT && lexer.startOfState != lexer.endOfLine { 158 | lexer.addSpan(lexer.startOfState, lexer.endOfLine, color) 159 | } 160 | 161 | # This may be after a contextual keyword 162 | else if previousIdentifier != null && previousIdentifier in _definitionKeywords { 163 | lexer.changePreviousSpanColor(.FOREGROUND_KEYWORD) 164 | } 165 | } 166 | } 167 | 168 | namespace Editor.Lexer { 169 | const SKEW = new(lexer => Syntax.SkewLexer.tokenize(lexer)) 170 | } 171 | -------------------------------------------------------------------------------- /src/themes/atom.sk: -------------------------------------------------------------------------------- 1 | namespace UI.Theme { 2 | const ATOM = Theme.new({ 3 | Color.BACKGROUND_DEFAULT: Graphics.RGBA.hex(0x282C34), 4 | Color.BACKGROUND_SELECTED: Graphics.RGBA.hex(0x3E4451), 5 | Color.FOREGROUND_COMMENT: Graphics.RGBA.hex(0x5C6370), 6 | Color.FOREGROUND_CONSTANT: Graphics.RGBA.hex(0xD19A66), 7 | Color.FOREGROUND_DEFAULT: Graphics.RGBA.hex(0xABB2BF), 8 | Color.FOREGROUND_DEFINITION: Graphics.RGBA.hex(0x61AFEF), 9 | Color.FOREGROUND_INSTANCE: Graphics.RGBA.hex(0xE06C75), 10 | Color.FOREGROUND_KEYWORD: Graphics.RGBA.hex(0xC678DD), 11 | Color.FOREGROUND_KEYWORD_CONSTANT: Graphics.RGBA.hex(0xD19A66), 12 | Color.FOREGROUND_MARGIN: Graphics.RGBA.hex(0x636D83), 13 | Color.FOREGROUND_MARGIN_HIGHLIGHTED: Graphics.RGBA.hex(0xABB2BF), 14 | Color.FOREGROUND_NUMBER: Graphics.RGBA.hex(0xD19A66), 15 | Color.FOREGROUND_PREPROCESSOR: Graphics.RGBA.hex(0xE06C75), 16 | Color.FOREGROUND_STRING: Graphics.RGBA.hex(0x98C379), 17 | Color.FOREGROUND_TYPE: Graphics.RGBA.hex(0xE5C07B), 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/themes/brackets.sk: -------------------------------------------------------------------------------- 1 | namespace UI.Theme { 2 | const BRACKETS_LIGHT = Theme.new({ 3 | Color.BACKGROUND_DEFAULT: Graphics.RGBA.hex(0xF8F8F8), 4 | Color.BACKGROUND_SELECTED: Graphics.RGBA.hex(0xABDFFA), 5 | Color.FOREGROUND_COMMENT: Graphics.RGBA.hex(0x949494), 6 | Color.FOREGROUND_DEFAULT: Graphics.RGBA.hex(0x535353), 7 | Color.FOREGROUND_DEFINITION: Graphics.RGBA.hex(0x8757AD), 8 | Color.FOREGROUND_KEYWORD: Graphics.RGBA.hex(0x446FBD), 9 | Color.FOREGROUND_KEYWORD_CONSTANT: Graphics.RGBA.hex(0xE88501), 10 | Color.FOREGROUND_MARGIN: Graphics.RGBA.hex(0x949494), 11 | Color.FOREGROUND_NUMBER: Graphics.RGBA.hex(0x6D8600), 12 | Color.FOREGROUND_STRING: Graphics.RGBA.hex(0xE88501), 13 | }) 14 | 15 | const BRACKETS_DARK = Theme.new({ 16 | Color.BACKGROUND_DEFAULT: Graphics.RGBA.hex(0x1D1F21), 17 | Color.BACKGROUND_SELECTED: Graphics.RGBA.hex(0x0050A0), 18 | Color.FOREGROUND_COMMENT: Graphics.RGBA.hex(0x767676), 19 | Color.FOREGROUND_DEFAULT: Graphics.RGBA.hex(0xDDDDDD), 20 | Color.FOREGROUND_DEFINITION: Graphics.RGBA.hex(0xB77FDB), 21 | Color.FOREGROUND_KEYWORD: Graphics.RGBA.hex(0x6C9EF8), 22 | Color.FOREGROUND_KEYWORD_CONSTANT: Graphics.RGBA.hex(0xD89333), 23 | Color.FOREGROUND_MARGIN: Graphics.RGBA.hex(0x767676), 24 | Color.FOREGROUND_NUMBER: Graphics.RGBA.hex(0x85A300), 25 | Color.FOREGROUND_STRING: Graphics.RGBA.hex(0xD89333), 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /src/themes/earthsong.sk: -------------------------------------------------------------------------------- 1 | namespace UI.Theme { 2 | const EARTHSONG = Theme.new({ 3 | Color.BACKGROUND_DEFAULT: Graphics.RGBA.hex(0x36312C), 4 | Color.BACKGROUND_MARGIN_HIGHLIGHTED: Graphics.RGBA.hex(0x45403B), 5 | Color.BACKGROUND_SELECTED: Graphics.RGBA.hex(0x574E45), 6 | Color.FOREGROUND_CARET: Graphics.RGBA.hex(0xF8F8F0), 7 | Color.FOREGROUND_COMMENT: Graphics.RGBA.hex(0x7A7267), 8 | Color.FOREGROUND_CONSTANT: Graphics.RGBA.hex(0xE6DB74), 9 | Color.FOREGROUND_DEFAULT: Graphics.RGBA.hex(0xEBD1B7), 10 | Color.FOREGROUND_DEFINITION: Graphics.RGBA.hex(0x60A365), 11 | Color.FOREGROUND_KEYWORD: Graphics.RGBA.hex(0xDB784D), 12 | Color.FOREGROUND_NUMBER: Graphics.RGBA.hex(0xF8BB39), 13 | Color.FOREGROUND_PREPROCESSOR: Graphics.RGBA.hex(0x75715E), 14 | Color.FOREGROUND_STRING: Graphics.RGBA.hex(0xF8BB39), 15 | Color.FOREGROUND_TYPE: Graphics.RGBA.hex(0x95CC5E), 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/themes/idle.sk: -------------------------------------------------------------------------------- 1 | namespace UI.Theme { 2 | const IDLE = Theme.new({ 3 | Color.BACKGROUND_DEFAULT: Graphics.RGBA.hex(0xFFFFFF), 4 | Color.BACKGROUND_SELECTED: Graphics.RGBA.hex(0xBEBEBE), 5 | Color.FOREGROUND_COMMENT: Graphics.RGBA.hex(0xDD0000), 6 | Color.FOREGROUND_CONSTANT: Graphics.RGBA.hex(0x900090), 7 | Color.FOREGROUND_DEFAULT: Graphics.RGBA.hex(0x000000), 8 | Color.FOREGROUND_DEFINITION: Graphics.RGBA.hex(0x0000FF), 9 | Color.FOREGROUND_KEYWORD: Graphics.RGBA.hex(0xFF7700), 10 | Color.FOREGROUND_KEYWORD_CONSTANT: Graphics.RGBA.hex(0x900090), 11 | Color.FOREGROUND_STRING: Graphics.RGBA.hex(0x00AA00), 12 | Color.FOREGROUND_TYPE: Graphics.RGBA.hex(0x770000), 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/themes/monokai.sk: -------------------------------------------------------------------------------- 1 | namespace UI.Theme { 2 | const MONOKAI = Theme.new({ 3 | Color.BACKGROUND_DEFAULT: Graphics.RGBA.hex(0x272822), 4 | Color.BACKGROUND_DIAGNOSTIC_ERROR: Graphics.RGBA.hex(0xFF2211), 5 | Color.BACKGROUND_MARGIN_HIGHLIGHTED: Graphics.RGBA.hex(0x3E3D32), 6 | Color.BACKGROUND_SELECTED: Graphics.RGBA.hex(0x49483E), 7 | Color.FOREGROUND_CARET: Graphics.RGBA.hex(0xF8F8F0), 8 | Color.FOREGROUND_COMMENT: Graphics.RGBA.hex(0x75715E), 9 | Color.FOREGROUND_CONSTANT: Graphics.RGBA.hex(0xAE81FF), 10 | Color.FOREGROUND_DEFAULT: Graphics.RGBA.hex(0xF8F8F2), 11 | Color.FOREGROUND_DEFINITION: Graphics.RGBA.hex(0xA6E22E), 12 | Color.FOREGROUND_KEYWORD: Graphics.RGBA.hex(0xF92672), 13 | Color.FOREGROUND_KEYWORD_CONSTANT: Graphics.RGBA.hex(0xAE81FF), 14 | Color.FOREGROUND_MARGIN: Graphics.RGBA.hex(0x8F908A), 15 | Color.FOREGROUND_NUMBER: Graphics.RGBA.hex(0xAE81FF), 16 | Color.FOREGROUND_PREPROCESSOR: Graphics.RGBA.hex(0xFD971F), 17 | Color.FOREGROUND_STRING: Graphics.RGBA.hex(0xE6DB74), 18 | Color.FOREGROUND_TYPE: Graphics.RGBA.hex(0x66D9EF), 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/themes/solarized.sk: -------------------------------------------------------------------------------- 1 | namespace UI.Theme { 2 | const SOLARIZED = Theme.new({ 3 | Color.BACKGROUND_DEFAULT: Graphics.RGBA.hex(0xFDF6E3), 4 | Color.BACKGROUND_DIAGNOSTIC_ERROR: Graphics.RGBA.hex(0xF1A680), 5 | Color.BACKGROUND_DIAGNOSTIC_WARNING: Graphics.RGBA.hex(0xE4D964), 6 | Color.BACKGROUND_MARGIN: Graphics.RGBA.hex(0xEEE8D5), 7 | Color.BACKGROUND_SELECTED: Graphics.RGBA.hex(0xEEE8D5), 8 | Color.FOREGROUND_CARET: Graphics.RGBA.hex(0x000000), 9 | Color.FOREGROUND_COMMENT: Graphics.RGBA.hex(0x93A1A1), 10 | Color.FOREGROUND_CONSTANT: Graphics.RGBA.hex(0xCB4B16), 11 | Color.FOREGROUND_DEFAULT: Graphics.RGBA.hex(0x586E75), 12 | Color.FOREGROUND_DEFINITION: Graphics.RGBA.hex(0x073642), 13 | Color.FOREGROUND_KEYWORD: Graphics.RGBA.hex(0x859900), 14 | Color.FOREGROUND_KEYWORD_CONSTANT: Graphics.RGBA.hex(0x859900), 15 | Color.FOREGROUND_NUMBER: Graphics.RGBA.hex(0xD33682), 16 | Color.FOREGROUND_STRING: Graphics.RGBA.hex(0x2AA198), 17 | Color.FOREGROUND_TYPE: Graphics.RGBA.hex(0x268BD2), 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/themes/twilight.sk: -------------------------------------------------------------------------------- 1 | namespace UI.Theme { 2 | const TWILIGHT = Theme.new({ 3 | Color.BACKGROUND_DEFAULT: Graphics.RGBA.hex(0x202020), 4 | Color.BACKGROUND_DIAGNOSTIC_ERROR: Graphics.RGBA.hex(0xFF2211), 5 | Color.BACKGROUND_MARGIN: Graphics.RGBA.hex(0x3E3E40), 6 | Color.BACKGROUND_MARGIN_HIGHLIGHTED: Graphics.RGBA.hex(0x535255), 7 | Color.BACKGROUND_SELECTED: Graphics.RGBA.hex(0x464A4C), 8 | Color.BORDER_MARGIN: Graphics.RGBA.hex(0x535255), 9 | Color.FOREGROUND_CARET: Graphics.RGBA.hex(0xA7A7A7), 10 | Color.FOREGROUND_COMMENT: Graphics.RGBA.hex(0x746D73), 11 | Color.FOREGROUND_CONSTANT: Graphics.RGBA.hex(0xB28349), 12 | Color.FOREGROUND_DEFAULT: Graphics.RGBA.hex(0xF8F8F8), 13 | Color.FOREGROUND_DEFINITION: Graphics.RGBA.hex(0xFAF4A0), 14 | Color.FOREGROUND_KEYWORD: Graphics.RGBA.hex(0xDCB777), 15 | Color.FOREGROUND_KEYWORD_CONSTANT: Graphics.RGBA.hex(0x8598B6), 16 | Color.FOREGROUND_MARGIN: Graphics.RGBA.hex(0xA0A0A0), 17 | Color.FOREGROUND_MARGIN_HIGHLIGHTED: Graphics.RGBA.hex(0xC6C6C6), 18 | Color.FOREGROUND_NUMBER: Graphics.RGBA.hex(0xE87854), 19 | Color.FOREGROUND_PREPROCESSOR: Graphics.RGBA.hex(0xB9CFE3), 20 | Color.FOREGROUND_STRING: Graphics.RGBA.hex(0x9AAF7C), 21 | Color.FOREGROUND_TYPE: Graphics.RGBA.hex(0xB195AD), 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/themes/visualstudio.sk: -------------------------------------------------------------------------------- 1 | namespace UI.Theme { 2 | const VISUAL_STUDIO_DARK = Theme.new({ 3 | Color.BACKGROUND_DEFAULT: Graphics.RGBA.hex(0x1E1E1E), 4 | Color.BACKGROUND_DIAGNOSTIC_ERROR: Graphics.RGBA.hex(0xFF2211), 5 | Color.BACKGROUND_MARGIN_HIGHLIGHTED: Graphics.RGBA.hex(0x0F0F0F), 6 | Color.BACKGROUND_SELECTED: Graphics.RGBA.hex(0x1E4F78), 7 | Color.FOREGROUND_COMMENT: Graphics.RGBA.hex(0x57A64A), 8 | Color.FOREGROUND_CONSTANT: Graphics.RGBA.hex(0xB8D7A3), 9 | Color.FOREGROUND_DEFAULT: Graphics.RGBA.hex(0xC8C8C8), 10 | Color.FOREGROUND_DEFINITION: Graphics.RGBA.hex(0x7F7F7F), 11 | Color.FOREGROUND_KEYWORD: Graphics.RGBA.hex(0x569CD6), 12 | Color.FOREGROUND_MARGIN: Graphics.RGBA.hex(0x2B91AF), 13 | Color.FOREGROUND_NUMBER: Graphics.RGBA.hex(0xB8D7A3), 14 | Color.FOREGROUND_PREPROCESSOR: Graphics.RGBA.hex(0xBD63C5), 15 | Color.FOREGROUND_STRING: Graphics.RGBA.hex(0xD69D85), 16 | Color.FOREGROUND_TYPE: Graphics.RGBA.hex(0x4EC9B0), 17 | }) 18 | 19 | const VISUAL_STUDIO_LIGHT = Theme.new({ 20 | Color.BACKGROUND_DEFAULT: Graphics.RGBA.hex(0xFFFFFF), 21 | Color.BACKGROUND_SELECTED: Graphics.RGBA.hex(0xADD6FF), 22 | Color.FOREGROUND_COMMENT: Graphics.RGBA.hex(0x008000), 23 | Color.FOREGROUND_DEFAULT: Graphics.RGBA.hex(0x000000), 24 | Color.FOREGROUND_KEYWORD: Graphics.RGBA.hex(0x0000FF), 25 | Color.FOREGROUND_MARGIN: Graphics.RGBA.hex(0x2B91AF), 26 | Color.FOREGROUND_PREPROCESSOR: Graphics.RGBA.hex(0x6F008A), 27 | Color.FOREGROUND_TYPE: Graphics.RGBA.hex(0x2B91AF), 28 | Color.FOREGROUND_STRING: Graphics.RGBA.hex(0xA31515), 29 | Color.FOREGROUND_DEFINITION: Graphics.RGBA.hex(0x808080), 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /src/themes/xcode.sk: -------------------------------------------------------------------------------- 1 | namespace UI.Theme { 2 | const XCODE = Theme.new({ 3 | Color.BACKGROUND_DEFAULT: Graphics.RGBA.hex(0xFFFFFF), 4 | Color.BACKGROUND_MARGIN: Graphics.RGBA.hex(0xF7F7F7), 5 | Color.BACKGROUND_SELECTED: Graphics.RGBA.hex(0xABD6FF), 6 | Color.BORDER_MARGIN: Graphics.RGBA.hex(0xBFBFBF, 0x5F), 7 | Color.FOREGROUND_ANNOTATION: Graphics.RGBA.hex(0x0E0EFF), 8 | Color.FOREGROUND_COMMENT: Graphics.RGBA.hex(0x007400), 9 | Color.FOREGROUND_CONSTANT: Graphics.RGBA.hex(0x25464A), 10 | Color.FOREGROUND_DEFAULT: Graphics.RGBA.hex(0x000000), 11 | Color.FOREGROUND_DEFINITION: Graphics.RGBA.hex(0x5B2599), 12 | Color.FOREGROUND_KEYWORD: Graphics.RGBA.hex(0xA90D91), 13 | Color.FOREGROUND_MARGIN: Graphics.RGBA.hex(0x929292), 14 | Color.FOREGROUND_NUMBER: Graphics.RGBA.hex(0x1C00CE), 15 | Color.FOREGROUND_PREPROCESSOR: Graphics.RGBA.hex(0x63381F), 16 | Color.FOREGROUND_STRING: Graphics.RGBA.hex(0xC41A15), 17 | Color.FOREGROUND_TYPE: Graphics.RGBA.hex(0x3E6D74), 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/ui/color.sk: -------------------------------------------------------------------------------- 1 | namespace UI { 2 | # Colors in the theme are identified semantically using these identifiers. 3 | # Most identifiers have a fallback identifier that will be substituted if 4 | # the theme doesn't override that identifier's color. 5 | enum Color { 6 | # Border colors 7 | BORDER_MARGIN 8 | 9 | # Background colors 10 | BACKGROUND_DEFAULT 11 | BACKGROUND_DIAGNOSTIC_ERROR 12 | BACKGROUND_DIAGNOSTIC_WARNING 13 | BACKGROUND_MARGIN 14 | BACKGROUND_MARGIN_HIGHLIGHTED 15 | BACKGROUND_SELECTED 16 | 17 | # Text colors 18 | FOREGROUND_CARET 19 | FOREGROUND_DEFAULT 20 | FOREGROUND_DIAGNOSTIC_ERROR 21 | FOREGROUND_DIAGNOSTIC_WARNING 22 | FOREGROUND_MARGIN 23 | FOREGROUND_MARGIN_HIGHLIGHTED 24 | 25 | # Syntax-specific colors 26 | FOREGROUND_ANNOTATION 27 | FOREGROUND_COMMENT 28 | FOREGROUND_CONSTANT 29 | FOREGROUND_DEFINITION 30 | FOREGROUND_INSTANCE 31 | FOREGROUND_KEYWORD 32 | FOREGROUND_KEYWORD_CONSTANT 33 | FOREGROUND_NUMBER 34 | FOREGROUND_PREPROCESSOR 35 | FOREGROUND_STRING 36 | FOREGROUND_TYPE 37 | } 38 | 39 | namespace Color { 40 | const FALLBACK IntMap = { 41 | BACKGROUND_MARGIN: BACKGROUND_DEFAULT, 42 | BACKGROUND_MARGIN_HIGHLIGHTED: BACKGROUND_MARGIN, 43 | FOREGROUND_ANNOTATION: FOREGROUND_KEYWORD, 44 | FOREGROUND_CARET: FOREGROUND_DEFAULT, 45 | FOREGROUND_COMMENT: FOREGROUND_DEFAULT, 46 | FOREGROUND_CONSTANT: FOREGROUND_DEFAULT, 47 | FOREGROUND_DEFINITION: FOREGROUND_DEFAULT, 48 | FOREGROUND_INSTANCE: FOREGROUND_DEFAULT, 49 | FOREGROUND_KEYWORD: FOREGROUND_DEFAULT, 50 | FOREGROUND_KEYWORD_CONSTANT: FOREGROUND_KEYWORD, 51 | FOREGROUND_MARGIN: FOREGROUND_DEFAULT, 52 | FOREGROUND_MARGIN_HIGHLIGHTED: FOREGROUND_MARGIN, 53 | FOREGROUND_NUMBER: FOREGROUND_DEFAULT, 54 | FOREGROUND_PREPROCESSOR: FOREGROUND_DEFAULT, 55 | FOREGROUND_STRING: FOREGROUND_DEFAULT, 56 | FOREGROUND_TYPE: FOREGROUND_DEFAULT, 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ui/event.sk: -------------------------------------------------------------------------------- 1 | namespace UI { 2 | flags Modifiers { 3 | ALT 4 | META 5 | SHIFT 6 | CONTROL 7 | } 8 | 9 | enum Key { 10 | NONE 11 | 12 | LETTER_A 13 | LETTER_B 14 | LETTER_C 15 | LETTER_D 16 | LETTER_E 17 | LETTER_F 18 | LETTER_G 19 | LETTER_H 20 | LETTER_I 21 | LETTER_J 22 | LETTER_K 23 | LETTER_L 24 | LETTER_M 25 | LETTER_N 26 | LETTER_O 27 | LETTER_P 28 | LETTER_Q 29 | LETTER_R 30 | LETTER_S 31 | LETTER_T 32 | LETTER_U 33 | LETTER_V 34 | LETTER_W 35 | LETTER_X 36 | LETTER_Y 37 | LETTER_Z 38 | 39 | NUMBER_0 40 | NUMBER_1 41 | NUMBER_2 42 | NUMBER_3 43 | NUMBER_4 44 | NUMBER_5 45 | NUMBER_6 46 | NUMBER_7 47 | NUMBER_8 48 | NUMBER_9 49 | 50 | ARROW_DOWN 51 | ARROW_LEFT 52 | ARROW_RIGHT 53 | ARROW_UP 54 | BACKSPACE 55 | COMMA 56 | DELETE 57 | END 58 | ENTER 59 | ESCAPE 60 | HOME 61 | PAGE_DOWN 62 | PAGE_UP 63 | PERIOD 64 | SEMICOLON 65 | SPACEBAR 66 | TAB 67 | } 68 | 69 | enum EventType { 70 | MOUSE_DOWN 71 | MOUSE_MOVE 72 | MOUSE_UP 73 | MOUSE_SCROLL 74 | 75 | KEY_DOWN 76 | KEY_UP 77 | 78 | FOCUS_ENTER 79 | FOCUS_LEAVE 80 | 81 | CLIPBOARD_CUT 82 | CLIPBOARD_COPY 83 | CLIPBOARD_PASTE 84 | 85 | TEXT 86 | 87 | def isMouseEvent bool { 88 | return self >= MOUSE_DOWN && self <= MOUSE_SCROLL 89 | } 90 | 91 | def isKeyEvent bool { 92 | return self >= KEY_DOWN && self <= KEY_UP 93 | } 94 | 95 | def isFocusEvent bool { 96 | return self >= FOCUS_ENTER && self <= FOCUS_LEAVE 97 | } 98 | 99 | def isClipboardEvent bool { 100 | return self >= CLIPBOARD_CUT && self <= CLIPBOARD_PASTE 101 | } 102 | 103 | def isTextEvent bool { 104 | return self == TEXT 105 | } 106 | } 107 | 108 | class Event { 109 | const type EventType 110 | const target View 111 | var _wasAccepted = false 112 | 113 | def wasAccepted bool { 114 | return _wasAccepted 115 | } 116 | 117 | def accept { 118 | _wasAccepted = true 119 | } 120 | 121 | def mouseEvent MouseEvent { 122 | assert(type.isMouseEvent && self is MouseEvent) 123 | return self as MouseEvent 124 | } 125 | 126 | def keyEvent KeyEvent { 127 | assert(type.isKeyEvent && self is KeyEvent) 128 | return self as KeyEvent 129 | } 130 | 131 | def clipboardEvent ClipboardEvent { 132 | assert(type.isClipboardEvent && self is ClipboardEvent) 133 | return self as ClipboardEvent 134 | } 135 | 136 | def textEvent TextEvent { 137 | assert(type.isTextEvent && self is TextEvent) 138 | return self as TextEvent 139 | } 140 | } 141 | 142 | class MouseEvent : Event { 143 | const locationInWindow Vector 144 | const modifiers Modifiers 145 | const clickCount int 146 | const delta Vector 147 | 148 | def locationInView(view View) Vector { 149 | var x = locationInWindow.x 150 | var y = locationInWindow.y 151 | 152 | while view != null { 153 | x -= view.bounds.x 154 | y -= view.bounds.y 155 | view = view.parent 156 | } 157 | 158 | return Vector.new(x, y) 159 | } 160 | } 161 | 162 | class KeyEvent : Event { 163 | const key Key 164 | const modifiers Modifiers 165 | } 166 | 167 | class ClipboardEvent : Event { 168 | var text string 169 | } 170 | 171 | class TextEvent : Event { 172 | var text string 173 | var isComposing bool 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/ui/font.sk: -------------------------------------------------------------------------------- 1 | namespace UI { 2 | enum Font { 3 | CODE_FONT 4 | MARGIN_FONT 5 | UI_FONT 6 | } 7 | 8 | flags FontFlags { 9 | BOLD 10 | ITALIC 11 | } 12 | 13 | def roundUpToNextTabStop(value double, spacing double) double { 14 | return Math.floor(value / spacing + 1) * spacing 15 | } 16 | 17 | interface FontInstance { 18 | def font Font 19 | def size double 20 | def lineHeight double 21 | def flags FontFlags 22 | def advanceWidth(codePoint int) double # This assumes a 1:1 mapping between glyphs and code points 23 | def renderGlyph(codePoint int) Graphics.Glyph # This may return null 24 | } 25 | 26 | # The current state is only valid after a call to "reset" and "moveNext" 27 | class AdvanceWidthIterator { 28 | const _unicodeIterator = Unicode.StringIterator.new 29 | var _font FontInstance = null 30 | var _advanceWidthFromLeft = 0.0 31 | var _spaceAdvanceWidth = 0.0 32 | var _tabStopSpacing = 0.0 33 | 34 | def currentIndex int { 35 | return _unicodeIterator.index 36 | } 37 | 38 | def advanceWidthFromLeft double { 39 | return _advanceWidthFromLeft 40 | } 41 | 42 | def reset(text string) { 43 | _unicodeIterator.reset(text, 0) 44 | _advanceWidthFromLeft = 0 45 | } 46 | 47 | def setFont(font FontInstance, indent int) { 48 | if _font != font { 49 | _font = font 50 | _spaceAdvanceWidth = font.advanceWidth(' ') 51 | } 52 | _tabStopSpacing = _spaceAdvanceWidth * indent 53 | } 54 | 55 | def nextCodePoint int { 56 | var codePoint = _unicodeIterator.nextCodePoint 57 | if codePoint != -1 { 58 | if codePoint == '\t' { 59 | _advanceWidthFromLeft = roundUpToNextTabStop(_advanceWidthFromLeft, _tabStopSpacing) 60 | } else { 61 | _advanceWidthFromLeft += _font.advanceWidth(codePoint) 62 | } 63 | } 64 | return codePoint 65 | } 66 | 67 | # This assumes the index is between code points, not in the middle of one 68 | def seekToIndex(index int) { 69 | var iterator = _unicodeIterator 70 | var oldIndex = iterator.index 71 | 72 | # No-op 73 | if index == oldIndex { 74 | return 75 | } 76 | 77 | # Scan forward 78 | if index > oldIndex { 79 | while iterator.index < index && nextCodePoint != -1 {} 80 | return 81 | } 82 | 83 | # Try scanning backward 84 | if index > oldIndex / 2 { 85 | while true { 86 | var codePoint = iterator.previousCodePoint 87 | 88 | # There's no way of knowing how wide a tab stop is by itself 89 | if codePoint == '\t' { 90 | iterator.index = 0 91 | break 92 | } 93 | 94 | # We're using doubles so error accumulation should be small 95 | _advanceWidthFromLeft -= _font.advanceWidth(codePoint) 96 | 97 | # Stop when the index was reached 98 | if iterator.index <= index { 99 | return 100 | } 101 | } 102 | } 103 | 104 | # If that didn't work, start over from the beginning and scan forward 105 | iterator.index = 0 106 | _advanceWidthFromLeft = 0 107 | while iterator.index < index && nextCodePoint != -1 {} 108 | } 109 | 110 | # Note: this will not seek backward 111 | def seekForwardToAdvanceWidth(advanceWidth double) { 112 | var previousAdvanceWidth = advanceWidthFromLeft 113 | var previousIndex = currentIndex 114 | 115 | # Seek up to and one past the query 116 | while advanceWidthFromLeft < advanceWidth { 117 | previousAdvanceWidth = advanceWidthFromLeft 118 | previousIndex = currentIndex 119 | 120 | # Don't run off the end of the string 121 | if nextCodePoint == -1 { 122 | break 123 | } 124 | } 125 | 126 | # Bisect the overlapped character at the center point 127 | if advanceWidth < (previousAdvanceWidth + advanceWidthFromLeft) / 2 { 128 | _unicodeIterator.index = previousIndex 129 | _advanceWidthFromLeft = previousAdvanceWidth 130 | } 131 | } 132 | } 133 | 134 | namespace AdvanceWidthIterator { 135 | const INSTANCE = new 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/ui/platform.sk: -------------------------------------------------------------------------------- 1 | namespace UI { 2 | enum OperatingSystem { 3 | ANDROID 4 | IOS 5 | LINUX 6 | OSX 7 | UNKNOWN 8 | WINDOWS 9 | } 10 | 11 | enum UserAgent { 12 | CHROME 13 | EDGE 14 | FIREFOX 15 | IE 16 | SAFARI 17 | UNKNOWN 18 | } 19 | 20 | interface Platform { 21 | def operatingSystem OperatingSystem 22 | def userAgent UserAgent 23 | def nowInSeconds double 24 | def createWindow Window # This may return null when called multiple times 25 | 26 | def baseModifier Modifiers { 27 | return operatingSystem == .OSX ? .META : .CONTROL 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ui/renderer.sk: -------------------------------------------------------------------------------- 1 | namespace UI { 2 | enum Cursor { 3 | ARROW 4 | TEXT 5 | } 6 | 7 | # This rendering interface is super simple so that it'll work for both a pixel- 8 | # based interface and a terminal-based interface. 9 | interface SemanticRenderer { 10 | # Getters 11 | def fontInstance(font Font) FontInstance 12 | 13 | # Commands 14 | def renderCaret(x double, y double, color Color) 15 | def renderHorizontalLine(x1 double, x2 double, y double, color Color) 16 | def renderRect(x double, y double, width double, height double, color Color) 17 | def renderRightwardShadow(x double, y double, width double, height double) 18 | def renderScrollbarThumb(x double, y double, width double, height double, color Color) 19 | def renderSquiggle(x double, y double, width double, height double, color Color) 20 | def renderText(x double, y double, text string, font Font, color Color, alpha int) 21 | def renderVerticalLine(x double, y1 double, y2 double, color Color) 22 | def renderView(view View) 23 | } 24 | 25 | # This rendering interface contains the minimal requirements for pixel-based 26 | # renderers. That way those renderers can just worry about rendering pixels 27 | # and not about translating semanting rendering commands. 28 | interface PixelRenderer { 29 | # Getters 30 | def width int 31 | def height int 32 | def pixelScale double 33 | def fontInstance(font Font) FontInstance 34 | 35 | # Commands 36 | def setViewport(x double, y double, width double, height double) 37 | def setDefaultBackgroundColor(color Graphics.RGBA) 38 | def fillRect(x double, y double, width double, height double, color Graphics.RGBA) 39 | def fillRoundedRect(x double, y double, width double, height double, color Graphics.RGBA, radius double) 40 | def strokePolyline(coordinates List, color Graphics.RGBA, thickness double) 41 | def renderText(x double, y double, text string, font Font, color Graphics.RGBA) 42 | def renderRectShadow( 43 | rectX double, rectY double, rectWidth double, rectHeight double, 44 | clipX double, clipY double, clipWidth double, clipHeight double, 45 | shadowAlpha double, blurSigma double) 46 | } 47 | 48 | # This contains all of the mappings from semantic rendering commands to pixels 49 | class SemanticToPixelTranslator :: SemanticRenderer { 50 | const _renderer PixelRenderer 51 | var _theme Theme = .FALLBACK 52 | var _viewport Rect = .EMPTY 53 | var lastCaretX = 0.0 54 | var lastCaretY = 0.0 55 | 56 | def fontInstance(font Font) FontInstance { 57 | return _renderer.fontInstance(font) 58 | } 59 | 60 | def renderView(view View) { 61 | var bounds = view.bounds 62 | var oldViewport = _viewport 63 | var left = oldViewport.left + Math.max(bounds.left, 0) 64 | var top = oldViewport.top + Math.max(bounds.top, 0) 65 | var right = oldViewport.left + Math.min(bounds.right, oldViewport.width) 66 | var bottom = oldViewport.top + Math.min(bounds.bottom, oldViewport.height) 67 | var newViewport = view == view.window.root ? view.bounds : Rect.new(left, top, right - left, bottom - top) 68 | 69 | _renderer.setViewport(newViewport.x, newViewport.y, newViewport.width, newViewport.height) 70 | _viewport = newViewport 71 | view.render 72 | _viewport = oldViewport 73 | } 74 | 75 | def setTheme(theme Theme) { 76 | _theme = theme 77 | _renderer.setDefaultBackgroundColor(theme.findColor(.BACKGROUND_DEFAULT)) 78 | } 79 | 80 | def renderRect(x double, y double, width double, height double, color Color) { 81 | _renderer.fillRect(x, y, width, height, _theme.findColor(color)) 82 | } 83 | 84 | def renderCaret(x double, y double, color Color) { 85 | lastCaretX = x + _viewport.x 86 | lastCaretY = y + _viewport.y 87 | _renderer.fillRect(x, y, 1, _renderer.fontInstance(.CODE_FONT).lineHeight, _theme.findColor(color)) 88 | } 89 | 90 | def renderSquiggle(x double, y double, width double, height double, color Color) { 91 | const SIZE = 2.0 92 | var flip = SIZE 93 | var coordinates List = [] 94 | 95 | x = Math.floor(x) + 0.5 96 | y = Math.floor(y + height * 0.9) + 0.5 97 | 98 | while width > 0 { 99 | coordinates.append(x) 100 | coordinates.append(y) 101 | x += SIZE 102 | y += flip 103 | width -= SIZE 104 | flip = -flip 105 | } 106 | 107 | _renderer.strokePolyline(coordinates, _theme.findColor(color), 1) 108 | } 109 | 110 | def renderRightwardShadow(x double, y double, width double, height double) { 111 | _renderer.renderRectShadow( 112 | x - 2 * width, y - width, 2 * width, height + 2 * width, 113 | x, y, width, height, 114 | 0.5, width / 3.0) 115 | } 116 | 117 | def renderText(x double, y double, text string, font Font, color Color, alpha int) { 118 | _renderer.renderText(x, y, text, font, _theme.findColor(color).multiplyAlpha(alpha)) 119 | } 120 | 121 | def renderHorizontalLine(x1 double, x2 double, y double, color Color) { 122 | assert(x1 <= x2) 123 | _renderer.fillRect(x1, y, x2 - x1, 1, _theme.findColor(color)) 124 | } 125 | 126 | def renderVerticalLine(x double, y1 double, y2 double, color Color) { 127 | assert(y1 <= y2) 128 | _renderer.fillRect(x, y1, 1, y2 - y1, _theme.findColor(color)) 129 | } 130 | 131 | def renderScrollbarThumb(x double, y double, width double, height double, color Color) { 132 | var padding = Math.round(Math.min(width, height) / 6) 133 | 134 | # Shrink the bounds by the padding 135 | x += padding 136 | y += padding 137 | width -= 2 * padding 138 | height -= 2 * padding 139 | 140 | _renderer.fillRoundedRect(x, y, width, height, _theme.findColor(color).multiplyAlpha(127), width / 2) 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/ui/theme.sk: -------------------------------------------------------------------------------- 1 | namespace UI { 2 | # A theme defines a color map for the view. All color identifiers have 3 | # default values either by having a theme fallback color directly or by 4 | # being substituted with another identifier that has a fallback color. 5 | class Theme { 6 | const colors IntMap 7 | 8 | def findColor(color UI.Color) Graphics.RGBA { 9 | while !(color in colors) { 10 | var next = UI.Color.FALLBACK.get(color, color) as UI.Color 11 | if next != color { 12 | color = next 13 | } else if self != FALLBACK { 14 | return FALLBACK.findColor(color) 15 | } else { 16 | return .TRANSPARENT 17 | } 18 | } 19 | return colors[color] 20 | } 21 | } 22 | 23 | namespace Theme { 24 | const FALLBACK = Theme.new({ 25 | UI.Color.BACKGROUND_DEFAULT: Graphics.RGBA.hex(0xFFFFFF), 26 | UI.Color.BACKGROUND_DIAGNOSTIC_ERROR: Graphics.RGBA.hex(0xFF8877), 27 | UI.Color.BACKGROUND_DIAGNOSTIC_WARNING: Graphics.RGBA.hex(0xFFEF00), 28 | UI.Color.BACKGROUND_SELECTED: Graphics.RGBA.hex(0xBFDFFF), 29 | UI.Color.BORDER_MARGIN: Graphics.RGBA.TRANSPARENT, 30 | UI.Color.FOREGROUND_DEFAULT: Graphics.RGBA.hex(0x000000), 31 | UI.Color.FOREGROUND_DIAGNOSTIC_ERROR: Graphics.RGBA.hex(0x000000), 32 | UI.Color.FOREGROUND_DIAGNOSTIC_WARNING: Graphics.RGBA.hex(0x000000), 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ui/view.sk: -------------------------------------------------------------------------------- 1 | namespace UI { 2 | enum LayoutKind { 3 | MIN 4 | STRETCH 5 | MAX 6 | } 7 | 8 | class AxisLayout { 9 | const kind LayoutKind 10 | const min double 11 | const max double 12 | 13 | def lower(size double) double { 14 | return kind == .MAX ? size + min : min 15 | } 16 | 17 | def upper(size double) double { 18 | return kind == .MIN ? max : size + max 19 | } 20 | } 21 | 22 | namespace AxisLayout { 23 | const STRETCH = AxisLayout.new(.STRETCH, 0, 0) 24 | } 25 | 26 | class View { 27 | const _window Window 28 | var _parent View = null 29 | var _bounds Rect = .EMPTY 30 | var _children FrozenList = List.new.freeze 31 | var _layoutX AxisLayout = .STRETCH 32 | var _layoutY AxisLayout = .STRETCH 33 | 34 | def window Window { 35 | return _window 36 | } 37 | 38 | def parent View { 39 | return _parent 40 | } 41 | 42 | def children FrozenList { 43 | return _children 44 | } 45 | 46 | def bounds Rect { 47 | return _bounds 48 | } 49 | 50 | def layoutX AxisLayout { 51 | return _layoutX 52 | } 53 | 54 | def layoutY AxisLayout { 55 | return _layoutY 56 | } 57 | 58 | def setLayout(kindX LayoutKind, minX double, maxX double, kindY LayoutKind, minY double, maxY double) { 59 | _layoutX = AxisLayout.new(kindX, minX, maxX) 60 | _layoutY = AxisLayout.new(kindY, minY, maxY) 61 | updateBounds 62 | } 63 | 64 | def handleEvent(event Event) { 65 | } 66 | 67 | def handleSizeChange { 68 | } 69 | 70 | def render { 71 | for i in 0..children.count { 72 | window.renderer.renderView(children[i]) 73 | } 74 | } 75 | 76 | def updateBounds { 77 | var parentSize = parent != null ? parent.bounds.size : self == window.root ? window.size : Vector.ZERO 78 | var minX = layoutX.lower(parentSize.x) 79 | var minY = layoutY.lower(parentSize.y) 80 | var maxX = layoutX.upper(parentSize.x) 81 | var maxY = layoutY.upper(parentSize.y) 82 | var newBounds = Rect.new(minX, minY, maxX - minX, maxY - minY) 83 | var oldBounds = bounds 84 | _bounds = newBounds 85 | 86 | if oldBounds.width != newBounds.width || oldBounds.height != newBounds.height { 87 | for i in 0..children.count { 88 | children[i].updateBounds 89 | } 90 | handleSizeChange 91 | } 92 | } 93 | 94 | def isParentOf(view View) bool { 95 | while view != null { 96 | view = view.parent 97 | if view == self { 98 | return true 99 | } 100 | } 101 | return false 102 | } 103 | 104 | def appendTo(parent View) { 105 | assert(self != parent) 106 | assert(self != window.root) 107 | assert(parent.window == window) 108 | assert(!isParentOf(parent)) 109 | removeFromParent 110 | var children = parent.children.thaw 111 | children.append(self) 112 | parent._children = children.freeze 113 | _parent = parent 114 | updateBounds 115 | } 116 | 117 | def removeFromParent { 118 | if parent != null { 119 | assert(self in parent.children) 120 | var children = parent.children.thaw 121 | children.removeOne(self) 122 | parent._children = children.freeze 123 | _parent = null 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/ui/window.sk: -------------------------------------------------------------------------------- 1 | namespace UI { 2 | interface WindowDelegate { 3 | def triggerAction(action Editor.Action) 4 | def triggerFrame 5 | } 6 | 7 | class Window { 8 | const _root View = null 9 | var _viewWithFocus View = null 10 | var _size Vector = .ZERO 11 | var _pixelScale = 0.0 12 | var _isInvalid = false 13 | var _isActive = true 14 | var _delegate WindowDelegate = null 15 | 16 | def new { 17 | _root = View.new(self) 18 | _viewWithFocus = _root 19 | } 20 | 21 | def platform Platform 22 | def renderer SemanticRenderer 23 | def setTitle(text string) 24 | def setTheme(theme Theme) 25 | def setCursor(cursor Cursor) 26 | def setFont(font Font, names List, size double, height double, flags FontFlags) 27 | def render 28 | 29 | def root View { 30 | return _root 31 | } 32 | 33 | def viewWithFocus View { 34 | return _viewWithFocus 35 | } 36 | 37 | def isActive bool { 38 | return _isActive 39 | } 40 | 41 | def size Vector { 42 | return _size 43 | } 44 | 45 | def pixelScale double { 46 | return _pixelScale 47 | } 48 | 49 | def invalidate { 50 | _isInvalid = true 51 | } 52 | 53 | def setDelegate(delegate WindowDelegate) { 54 | _delegate = delegate 55 | } 56 | 57 | def focusView(view View) { 58 | assert(view != null) 59 | var old = _viewWithFocus 60 | if old != view { 61 | _viewWithFocus = view 62 | dispatchEvent(Event.new(.FOCUS_LEAVE, old)) 63 | assert(_viewWithFocus == view) 64 | dispatchEvent(Event.new(.FOCUS_ENTER, view)) 65 | } 66 | } 67 | 68 | def viewFromLocation(locationInWindow Vector) View { 69 | var visit fn(View, Vector) View = (view, location) => { 70 | for i = view.children.count - 1; i >= 0; i-- { 71 | var child = view.children[i] 72 | if child.bounds.contains(location) { 73 | return visit(child, location - child.bounds.topLeft) 74 | } 75 | } 76 | return view 77 | } 78 | return visit(root, locationInWindow) 79 | } 80 | 81 | def dispatchEvent(event Event) View { 82 | var target = event.target 83 | while target != null { 84 | target.handleEvent(event) 85 | if event.wasAccepted { 86 | break 87 | } 88 | target = target.parent 89 | } 90 | return target 91 | } 92 | 93 | def _handleResize(size Vector, pixelScale double) { 94 | assert(size.x >= 0) 95 | assert(size.y >= 0) 96 | assert(pixelScale > 0) 97 | 98 | if !_size.equals(size) || _pixelScale != pixelScale { 99 | _size = size 100 | _pixelScale = pixelScale 101 | _root.updateBounds 102 | invalidate 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /terminal/terminal.cpp: -------------------------------------------------------------------------------- 1 | #define SKEW_GC_MARK_AND_SWEEP 2 | #define SKEW_GC_PARALLEL 3 | #define _XOPEN_SOURCE_EXTENDED 4 | #include 5 | 6 | namespace Log { 7 | void info(const Skew::string &text) {} 8 | void warning(const Skew::string &text) {} 9 | void error(const Skew::string &text) {} 10 | } 11 | 12 | #include "compiled.cpp" 13 | #include 14 | #include 15 | 16 | // The "ncurses" library is in a platform-dependent location 17 | #if __linux__ 18 | #include // Install: "apt-get install libncursesw5-dev" 19 | #else 20 | #include // Comes with OS X? 21 | #endif 22 | 23 | //////////////////////////////////////////////////////////////////////////////// 24 | 25 | enum { 26 | SKY_COLOR_COMMENT = 1, 27 | SKY_COLOR_CONSTANT = 2, 28 | SKY_COLOR_KEYWORD = 3, 29 | SKY_COLOR_MARGIN = 4, 30 | SKY_COLOR_SELECTED = 5, 31 | SKY_COLOR_STRING = 6, 32 | }; 33 | 34 | // Using wchar_t for unicode was the worst idea ever 35 | void codePointToWchars(int codePoint, cchar_t &buffer) { 36 | memset(buffer.chars, 0, sizeof(buffer.chars)); 37 | 38 | // UTF-32 39 | if (sizeof(wchar_t) == 4) { 40 | buffer.chars[0] = codePoint; 41 | } 42 | 43 | // UTF-16 44 | else if (sizeof(wchar_t) == 2) { 45 | if (codePoint < 0x10000) { 46 | buffer.chars[0] = codePoint; 47 | } else { 48 | buffer.chars[0] = ((codePoint - 0x10000) >> 10) + 0xD800; 49 | buffer.chars[1] = ((codePoint - 0x10000) & ((1 << 10) - 1)) + 0xDC00; 50 | } 51 | } 52 | 53 | // UTF-8 54 | else { 55 | static_assert(sizeof(wchar_t) == 2 || sizeof(wchar_t) == 4, "unsupported platform"); 56 | } 57 | } 58 | 59 | namespace Terminal { 60 | struct FontInstance : UI::FontInstance { 61 | FontInstance(UI::Font font) : _font(font) { 62 | } 63 | 64 | virtual UI::Font font() override { 65 | return _font; 66 | } 67 | 68 | virtual double size() override { 69 | return 1; 70 | } 71 | 72 | virtual double lineHeight() override { 73 | return 1; 74 | } 75 | 76 | virtual int flags() override { 77 | return 0; 78 | } 79 | 80 | // A single character can actually take up multiple cells in the terminal 81 | // (or no cells at all if libc decides it doesn't feel like printing it) 82 | virtual double advanceWidth(int codePoint) override { 83 | auto it = _advanceWidths.find(codePoint); 84 | if (it != _advanceWidths.end()) { 85 | return it->second; 86 | } 87 | cchar_t buffer; 88 | codePointToWchars(codePoint, buffer); 89 | return _advanceWidths[codePoint] = std::max(0, wcswidth(buffer.chars, sizeof(buffer.chars) / sizeof(*buffer.chars))); 90 | } 91 | 92 | virtual Graphics::Glyph *renderGlyph(int codePoint) override { 93 | return nullptr; 94 | } 95 | 96 | #ifdef SKEW_GC_MARK_AND_SWEEP 97 | virtual void __gc_mark() override { 98 | UI::FontInstance::__gc_mark(); 99 | } 100 | #endif 101 | 102 | private: 103 | UI::Font _font = {}; 104 | std::unordered_map _advanceWidths; 105 | }; 106 | 107 | struct Host : UI::Platform, UI::Window, private UI::SemanticRenderer { 108 | struct ClipRect { 109 | int minX; 110 | int minY; 111 | int maxX; 112 | int maxY; 113 | }; 114 | 115 | bool triggerFrame() { 116 | if (_delegate != nullptr) { 117 | _delegate->triggerFrame(); 118 | } 119 | bool wasInvalid = _isInvalid; 120 | _isInvalid = false; 121 | return wasInvalid; 122 | } 123 | 124 | void handleResize() { 125 | _width = getmaxx(stdscr); 126 | _height = getmaxy(stdscr); 127 | _handleResize(new Vector(_width, _height), 1); 128 | } 129 | 130 | void render() { 131 | erase(); 132 | 133 | // Let the root view render itself 134 | assert(_clipRectStack.empty()); 135 | _clipRectStack.push_back(ClipRect{0, 0, _width, _height}); 136 | _root->render(); 137 | _clipRectStack.pop_back(); 138 | 139 | // Make sure dead characters on OS X (such as the umlaut) don't shift stuff over 140 | move(_height - 1, _width - 1); 141 | 142 | // The ncurses library will compute the minimum terminal commands to update the screen 143 | refresh(); 144 | 145 | #ifdef SKEW_GC_PARALLEL 146 | Skew::GC::parallelCollect(); 147 | #else 148 | Skew::GC::blockingCollect(); 149 | #endif 150 | } 151 | 152 | void triggerAction(Editor::Action action) { 153 | switch (action) { 154 | case Editor::Action::CUT: 155 | case Editor::Action::COPY: 156 | case Editor::Action::PASTE: { 157 | auto text = _readFromClipboard(); 158 | auto type = 159 | action == Editor::Action::CUT ? UI::EventType::CLIPBOARD_CUT : 160 | action == Editor::Action::COPY ? UI::EventType::CLIPBOARD_COPY : 161 | UI::EventType::CLIPBOARD_PASTE; 162 | auto event = new UI::ClipboardEvent(type, viewWithFocus(), text); 163 | dispatchEvent(event); 164 | if (event->text != text) { 165 | _writeToClipboard(event->text.std_str()); 166 | } 167 | break; 168 | } 169 | 170 | default: { 171 | if (_delegate != nullptr) { 172 | _delegate->triggerAction(action); 173 | } 174 | break; 175 | } 176 | } 177 | } 178 | 179 | void insertUTF8(char c) { 180 | dispatchEvent(new UI::TextEvent(UI::EventType::TEXT, viewWithFocus(), std::string(1, c), false)); 181 | } 182 | 183 | virtual UI::OperatingSystem operatingSystem() override { 184 | return UI::OperatingSystem::UNKNOWN; 185 | } 186 | 187 | virtual UI::UserAgent userAgent() override { 188 | return UI::UserAgent::UNKNOWN; 189 | } 190 | 191 | virtual double nowInSeconds() override { 192 | timeval data; 193 | gettimeofday(&data, nullptr); 194 | return data.tv_sec + data.tv_usec / 1.0e6; 195 | } 196 | 197 | virtual UI::Window *createWindow() override { 198 | return this; 199 | } 200 | 201 | virtual UI::SemanticRenderer *renderer() override { 202 | return this; 203 | } 204 | 205 | virtual UI::Platform *platform() override { 206 | return this; 207 | } 208 | 209 | virtual UI::FontInstance *fontInstance(UI::Font font) override { 210 | auto it = _fontInstances.find((int)font); 211 | if (it != _fontInstances.end()) { 212 | return it->second; 213 | } 214 | return _fontInstances[(int)font] = new FontInstance(font); 215 | } 216 | 217 | virtual void setFont(UI::Font font, Skew::List *names, double size, double height, int flags) override { 218 | } 219 | 220 | virtual void setTitle(Skew::string title) override { 221 | } 222 | 223 | virtual void setTheme(UI::Theme *theme) override { 224 | } 225 | 226 | virtual void setCursor(UI::Cursor cursor) override { 227 | } 228 | 229 | virtual void renderView(UI::View *view) override { 230 | assert(!_clipRectStack.empty()); 231 | auto clip = _clipRectStack.back(); 232 | auto bounds = view->bounds(); 233 | 234 | _clipRectStack.push_back(ClipRect{ 235 | clip.minX + std::max((int)bounds->x, 0), 236 | clip.minY + std::max((int)bounds->y, 0), 237 | clip.minX + std::min((int)(bounds->x + bounds->width), clip.maxX - clip.minX), 238 | clip.minY + std::min((int)(bounds->y + bounds->height), clip.maxY - clip.minY), 239 | }); 240 | view->render(); 241 | _clipRectStack.pop_back(); 242 | } 243 | 244 | virtual void renderRect(double x, double y, double width, double height, UI::Color color) override { 245 | assert(x == (int)x); 246 | assert(y == (int)y); 247 | assert(width == (int)width); 248 | assert(height == (int)height); 249 | 250 | auto clip = _clipRectStack.back(); 251 | x += clip.minX; 252 | y += clip.minY; 253 | 254 | if (color == UI::Color::BACKGROUND_SELECTED || color == UI::Color::BACKGROUND_MARGIN) { 255 | int minX = std::max(clip.minX, (int)x); 256 | int minY = std::max(clip.minY, (int)y); 257 | int maxX = std::min(clip.maxX, (int)(x + width)); 258 | int maxY = std::min(clip.maxY, (int)(y + height)); 259 | int n = std::max(0, maxX - minX); 260 | cchar_t buffer; 261 | 262 | for (int y = minY; y < maxY; y++) { 263 | for (int x = 0; x < n; x++) { 264 | if (color == UI::Color::BACKGROUND_SELECTED) { 265 | mvin_wch(y, minX + x, &buffer); 266 | buffer.attr = (buffer.attr & ~A_COLOR) | COLOR_PAIR(SKY_COLOR_SELECTED); 267 | } else { 268 | memset(&buffer, 0, sizeof(buffer)); 269 | buffer.chars[0] = ' '; 270 | } 271 | mvadd_wch(y, minX + x, &buffer); 272 | } 273 | } 274 | } 275 | } 276 | 277 | virtual void renderCaret(double x, double y, UI::Color color) override { 278 | assert(x == (int)x); 279 | assert(y == (int)y); 280 | 281 | auto clip = _clipRectStack.back(); 282 | x += clip.minX; 283 | y += clip.minY; 284 | 285 | if (x < clip.minX || y < clip.minY || x >= clip.maxX || y >= clip.maxY) { 286 | return; 287 | } 288 | 289 | // Underline the character at (x, y) 290 | cchar_t c; 291 | mvin_wch(y, x, &c); 292 | c.attr |= A_UNDERLINE; 293 | mvadd_wch(y, x, &c); 294 | } 295 | 296 | virtual void renderSquiggle(double x, double y, double width, double height, UI::Color color) override { 297 | } 298 | 299 | virtual void renderRightwardShadow(double x, double y, double width, double height) override { 300 | } 301 | 302 | virtual void renderText(double x, double y, Skew::string text, UI::Font font, UI::Color color, int alpha) override { 303 | assert(x == (int)x); 304 | assert(y == (int)y); 305 | 306 | auto clip = _clipRectStack.back(); 307 | x += clip.minX; 308 | y += clip.minY; 309 | 310 | if (y < clip.minY || y >= clip.maxY) { 311 | return; 312 | } 313 | 314 | auto instance = fontInstance(font); 315 | bool isMargin = color == UI::Color::FOREGROUND_MARGIN || color == UI::Color::FOREGROUND_MARGIN_HIGHLIGHTED; 316 | int attributes = 317 | isMargin ? COLOR_PAIR(SKY_COLOR_MARGIN) : 318 | color == UI::Color::FOREGROUND_KEYWORD || color == UI::Color::FOREGROUND_KEYWORD_CONSTANT ? COLOR_PAIR(SKY_COLOR_KEYWORD) : 319 | color == UI::Color::FOREGROUND_CONSTANT || color == UI::Color::FOREGROUND_NUMBER ? COLOR_PAIR(SKY_COLOR_CONSTANT) : 320 | color == UI::Color::FOREGROUND_COMMENT ? COLOR_PAIR(SKY_COLOR_COMMENT) : 321 | color == UI::Color::FOREGROUND_STRING ? COLOR_PAIR(SKY_COLOR_STRING) : 322 | color == UI::Color::FOREGROUND_DEFINITION ? A_BOLD : 323 | 0; 324 | 325 | auto utf32 = codePointsFromString(text); 326 | int start = std::max(0, std::min(utf32->count(), clip.minX - (int)x)); 327 | int end = std::max(0, std::min(utf32->count(), clip.maxX - (int)x)); 328 | int n = std::max(0, end - start); 329 | cchar_t buffer; 330 | 331 | // Merge the characters with the background attributes already present 332 | for (int i = 0; i < n; i++) { 333 | mvin_wch(y, x + start, &buffer); 334 | int codePoint = (*utf32)[start + i]; 335 | 336 | // Make sure tab doesn't mess stuff up 337 | if (codePoint == '\t') { 338 | codePoint = ' '; 339 | } 340 | 341 | // Non-printable characters (according to libc) will have an advance width of 0 342 | int advanceWidth = instance->advanceWidth(codePoint); 343 | if (advanceWidth > 0) { 344 | // Transfer the text color 345 | int color = buffer.attr & A_COLOR; 346 | buffer.attr = PAIR_NUMBER(color) == 0 ? attributes : color | (attributes & A_BOLD); 347 | 348 | // Make sure rendering a space doesn't cover up the character underneath 349 | if (codePoint != ' ') { 350 | codePointToWchars(codePoint, buffer); 351 | } 352 | 353 | // Write over the contents at (x, y) 354 | mvadd_wch(y, x + start, &buffer); 355 | x += advanceWidth; 356 | } 357 | } 358 | } 359 | 360 | virtual void renderHorizontalLine(double x1, double x2, double y, UI::Color color) override { 361 | } 362 | 363 | virtual void renderVerticalLine(double x, double y1, double y2, UI::Color color) override { 364 | } 365 | 366 | virtual void renderScrollbarThumb(double x, double y, double width, double height, UI::Color color) override { 367 | } 368 | 369 | #ifdef SKEW_GC_MARK_AND_SWEEP 370 | virtual void __gc_mark() override { 371 | UI::Platform::__gc_mark(); 372 | UI::Window::__gc_mark(); 373 | UI::SemanticRenderer::__gc_mark(); 374 | 375 | for (const auto &it : _fontInstances) { 376 | Skew::GC::mark(it.second); 377 | } 378 | } 379 | #endif 380 | 381 | private: 382 | void _writeToCommand(const char *command, const std::string &text) { 383 | if (FILE *f = popen(command, "w")) { 384 | fputs(text.c_str(), f); 385 | pclose(f); 386 | } 387 | } 388 | 389 | void _readFromCommand(const char *command) { 390 | if (FILE *f = popen(command, "r")) { 391 | char chunk[1024]; 392 | std::string buffer; 393 | while (fgets(chunk, sizeof(chunk), f)) { 394 | buffer += chunk; 395 | } 396 | if (!pclose(f)) { 397 | _clipboard = std::move(buffer); 398 | } 399 | } 400 | } 401 | 402 | void _writeToClipboard(std::string text) { 403 | _writeToCommand("pbcopy", text); 404 | _writeToCommand("xclip -i -selection clipboard", text); 405 | _clipboard = std::move(text); 406 | } 407 | 408 | std::string _readFromClipboard() { 409 | _readFromCommand("pbpaste -Prefer text"); 410 | _readFromCommand("xclip -o -selection clipboard"); 411 | return _clipboard; 412 | } 413 | 414 | int _width = 0; 415 | int _height = 0; 416 | std::string _clipboard; 417 | std::vector _buffer; 418 | std::vector _clipRectStack; 419 | std::unordered_map _fontInstances; 420 | }; 421 | } 422 | 423 | //////////////////////////////////////////////////////////////////////////////// 424 | 425 | static void handleEscapeSequence(Terminal::Host *host) { 426 | static std::unordered_map map = { 427 | { "[", Editor::Action::SELECT_FIRST_REGION }, 428 | { "[3~", Editor::Action::DELETE_RIGHT_CHARACTER }, 429 | { "[5~", Editor::Action::MOVE_UP_PAGE }, 430 | { "[6~", Editor::Action::MOVE_DOWN_PAGE }, 431 | { "[A", Editor::Action::MOVE_UP_LINE }, 432 | { "[B", Editor::Action::MOVE_DOWN_LINE }, 433 | { "[C", Editor::Action::MOVE_RIGHT_CHARACTER }, 434 | { "[D", Editor::Action::MOVE_LEFT_CHARACTER }, 435 | { "[F", Editor::Action::MOVE_RIGHT_LINE }, 436 | { "[H", Editor::Action::MOVE_LEFT_LINE }, 437 | }; 438 | std::string sequence; 439 | 440 | // Escape sequences can have arbitrary length 441 | do { 442 | int c = getch(); 443 | 444 | // If getch times out, this was just a plain escape 445 | if (c == -1) { 446 | break; 447 | } 448 | 449 | sequence += c; 450 | } while (sequence == "[" || sequence[sequence.size() - 1] < '@'); 451 | 452 | // Dispatch an action if there's a match 453 | auto it = map.find(sequence); 454 | if (it != map.end()) { 455 | host->triggerAction(it->second); 456 | } 457 | } 458 | 459 | int main() { 460 | // Don't let errors from clipboard commands crap all over the editor UI 461 | fclose(stderr); 462 | 463 | // We want unicode support 464 | setlocale(LC_ALL, "en_US.UTF-8"); 465 | 466 | // Let the ncurses library take over the screen and make sure it's cleaned up 467 | initscr(); 468 | atexit([] { endwin(); }); 469 | 470 | // Prepare colors 471 | start_color(); 472 | use_default_colors(); 473 | init_pair(SKY_COLOR_MARGIN, COLOR_BLUE, -1); 474 | init_pair(SKY_COLOR_KEYWORD, COLOR_RED, -1); 475 | init_pair(SKY_COLOR_COMMENT, COLOR_CYAN, -1); 476 | init_pair(SKY_COLOR_STRING, COLOR_GREEN, -1); 477 | init_pair(SKY_COLOR_CONSTANT, COLOR_MAGENTA, -1); 478 | init_pair(SKY_COLOR_SELECTED, COLOR_BLACK, COLOR_YELLOW); 479 | 480 | // More setup 481 | raw(); // Don't automatically generate any signals 482 | noecho(); // Don't auto-print typed characters 483 | curs_set(0); // Hide the cursor since we have our own carets 484 | timeout(10); // Don't let getch() block too long 485 | 486 | bool isInvalid = false; 487 | Skew::Root host(new Terminal::Host); 488 | Skew::Root app(new Editor::App(host.get())); 489 | Skew::Root shortcuts(new Editor::ShortcutMap(host.get())); 490 | 491 | host->handleResize(); 492 | host->render(); 493 | 494 | while (true) { 495 | int c = getch(); 496 | 497 | // Handle getch() timeout 498 | if (c == -1) { 499 | if (isInvalid) { 500 | host->render(); 501 | isInvalid = false; 502 | } else if (host->triggerFrame()) { 503 | isInvalid = true; 504 | } 505 | continue; 506 | } 507 | 508 | // Handle escape sequences 509 | if (c == 27) { 510 | handleEscapeSequence(host); 511 | } 512 | 513 | // Special-case the Control+Q shortcut to quit 514 | else if (c == 'Q' - 'A' + 1) { 515 | break; 516 | } 517 | 518 | // Handle shortcuts using control characters 519 | else if (c < 32 && c != '\n' && c != '\t') { 520 | auto key = c == 0 ? UI::Key::SPACEBAR : c <= 26 ? (UI::Key)((int)UI::Key::LETTER_A + c - 1) : UI::Key::NONE; 521 | auto action = shortcuts->get(key, UI::Modifiers::CONTROL); 522 | if (action != Editor::Action::NONE) { 523 | host->triggerAction(action); 524 | } 525 | } 526 | 527 | // Special-case tab 528 | else if (c == '\t') { 529 | host->triggerAction(Editor::Action::INSERT_TAB_FORWARD); 530 | } 531 | 532 | // Special-case backspace 533 | else if (c == 127) { 534 | host->triggerAction(Editor::Action::DELETE_LEFT_CHARACTER); 535 | } 536 | 537 | // Handle regular typed text 538 | else if ((c >= 32 && c <= 0xFF) || c == '\n') { 539 | host->insertUTF8(c); 540 | } 541 | 542 | // Was the terminal resized? 543 | else if (c == KEY_RESIZE) { 544 | host->handleResize(); 545 | resizeterm(host->size()->y, host->size()->x); 546 | host->render(); 547 | continue; 548 | } 549 | 550 | // Only render after an idle delay in case there's a lot of input 551 | isInvalid = true; 552 | } 553 | 554 | return 0; 555 | } 556 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | --------------------------------------------------------------------------------