├── .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 |
--------------------------------------------------------------------------------