├── resources ├── dido.png ├── dub.png ├── run.png ├── test.png ├── build.png ├── corner.png ├── cycle.png └── dlang.png ├── screenshots └── dido.jpg ├── fonts ├── SourceCodePro-Bold.ttf ├── SourceCodePro-Black.ttf ├── SourceCodePro-Light.ttf ├── SourceCodePro-Medium.ttf ├── SourceCodePro-Regular.ttf ├── SourceCodePro-Semibold.ttf ├── SourceCodePro-ExtraLight.ttf └── LICENSE.txt ├── source ├── dido │ ├── config.d │ ├── gui │ │ ├── package.d │ │ ├── context.d │ │ ├── button.d │ │ ├── combobox.d │ │ ├── scrollbar.d │ │ ├── font.d │ │ └── element.d │ ├── panel │ │ ├── package.d │ │ ├── mainpanel.d │ │ ├── images.d │ │ ├── solutionpanel.d │ │ ├── westpanel.d │ │ ├── outputpanel.d │ │ ├── commandlinepanel.d │ │ ├── menupanel.d │ │ └── textarea.d │ ├── buffer │ │ ├── analysis.d │ │ ├── buffercommand.d │ │ ├── bufferiterator.d │ │ ├── selection.d │ │ └── buffer.d │ ├── colors.d │ ├── window.d │ ├── project.d │ ├── builder.d │ ├── app.d │ ├── engine.d │ └── pngload.d └── main.d ├── dub.json ├── README.md └── UNLICENSE.txt /resources/dido.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0nce/dido/HEAD/resources/dido.png -------------------------------------------------------------------------------- /resources/dub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0nce/dido/HEAD/resources/dub.png -------------------------------------------------------------------------------- /resources/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0nce/dido/HEAD/resources/run.png -------------------------------------------------------------------------------- /resources/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0nce/dido/HEAD/resources/test.png -------------------------------------------------------------------------------- /resources/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0nce/dido/HEAD/resources/build.png -------------------------------------------------------------------------------- /resources/corner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0nce/dido/HEAD/resources/corner.png -------------------------------------------------------------------------------- /resources/cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0nce/dido/HEAD/resources/cycle.png -------------------------------------------------------------------------------- /resources/dlang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0nce/dido/HEAD/resources/dlang.png -------------------------------------------------------------------------------- /screenshots/dido.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0nce/dido/HEAD/screenshots/dido.jpg -------------------------------------------------------------------------------- /fonts/SourceCodePro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0nce/dido/HEAD/fonts/SourceCodePro-Bold.ttf -------------------------------------------------------------------------------- /fonts/SourceCodePro-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0nce/dido/HEAD/fonts/SourceCodePro-Black.ttf -------------------------------------------------------------------------------- /fonts/SourceCodePro-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0nce/dido/HEAD/fonts/SourceCodePro-Light.ttf -------------------------------------------------------------------------------- /fonts/SourceCodePro-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0nce/dido/HEAD/fonts/SourceCodePro-Medium.ttf -------------------------------------------------------------------------------- /fonts/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0nce/dido/HEAD/fonts/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /fonts/SourceCodePro-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0nce/dido/HEAD/fonts/SourceCodePro-Semibold.ttf -------------------------------------------------------------------------------- /fonts/SourceCodePro-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0nce/dido/HEAD/fonts/SourceCodePro-ExtraLight.ttf -------------------------------------------------------------------------------- /source/dido/config.d: -------------------------------------------------------------------------------- 1 | module dido.config; 2 | 3 | 4 | class DidoConfig 5 | { 6 | public: 7 | 8 | string fontFace = "fonts/SourceCodePro-Semibold.ttf"; 9 | int fontSize = 20; 10 | 11 | this() 12 | { 13 | } 14 | } -------------------------------------------------------------------------------- /source/dido/gui/package.d: -------------------------------------------------------------------------------- 1 | module dido.gui; 2 | 3 | public import dido.gui.element; 4 | public import dido.gui.context; 5 | public import dido.gui.font; 6 | public import dido.gui.scrollbar; 7 | public import dido.gui.combobox; 8 | public import dido.gui.button; 9 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dido", 3 | 4 | "targetType": "executable", 5 | "importPaths": ["source"], 6 | "sourcePaths": ["source"], 7 | "stringImportPaths": ["resources"], 8 | 9 | "dependencies": { 10 | "gfm:sdl2": "~>6.0", 11 | "gfm:math": "~>6.0", 12 | "scheme-d": "~>1.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /source/dido/panel/package.d: -------------------------------------------------------------------------------- 1 | module dido.panel; 2 | 3 | // This is a custom-made GUI for dido 4 | // reflow and render is pretty manual 5 | 6 | public import dido.panel.mainpanel; 7 | public import dido.panel.menupanel; 8 | public import dido.panel.commandlinepanel; 9 | public import dido.panel.textarea; 10 | public import dido.panel.solutionpanel; 11 | public import dido.panel.outputpanel; 12 | public import dido.panel.westpanel; 13 | -------------------------------------------------------------------------------- /source/dido/buffer/analysis.d: -------------------------------------------------------------------------------- 1 | module dido.buffer.analysis; 2 | 3 | //import std.d.lexer; 4 | 5 | /+ 6 | class Analysis 7 | { 8 | this(string filename, string source) 9 | { 10 | _filename = filename; 11 | _stringCache = StringCache(StringCache.defaultBucketCount); 12 | } 13 | 14 | void parse(string filemane, string source) 15 | { 16 | _tokens = getTokensForParser( cast(ubyte[]) source, LexerConfig(filemane, StringBehavior.source), &_stringCache); 17 | } 18 | 19 | private: 20 | string _filename; 21 | StringCache _stringCache; 22 | const(Token)[] _tokens; 23 | }+/ -------------------------------------------------------------------------------- /source/dido/panel/mainpanel.d: -------------------------------------------------------------------------------- 1 | module dido.panel.mainpanel; 2 | 3 | import dido.gui; 4 | 5 | class MainPanel : UIElement 6 | { 7 | public: 8 | this(UIContext context) 9 | { 10 | super(context); 11 | } 12 | 13 | override void reflow(box2i availableSpace) 14 | { 15 | _position = availableSpace; 16 | 17 | // reflow horizontal bars first 18 | child(4).reflow(availableSpace); 19 | 20 | 21 | child(3).reflow(availableSpace); 22 | availableSpace.min.y = child(3).position.max.y; 23 | 24 | child(2).reflow(availableSpace); 25 | availableSpace.max.y = child(2).position.min.y; 26 | 27 | int widthOfWestPanel = (250 + availableSpace.width / 3) / 2; 28 | 29 | box2i spaceForWest = availableSpace; 30 | spaceForWest.max.x = widthOfWestPanel; 31 | 32 | child(1).reflow(spaceForWest); 33 | 34 | 35 | availableSpace.min.x = widthOfWestPanel; 36 | child(0).reflow(availableSpace); 37 | } 38 | } 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /source/dido/panel/images.d: -------------------------------------------------------------------------------- 1 | module dido.panel.images; 2 | 3 | import dido.gui.context; 4 | 5 | static immutable imageDido = cast(immutable(ubyte[])) import("dido.png"); 6 | static immutable imageCorner = cast(immutable(ubyte[])) import("corner.png"); 7 | static immutable imageBuild = cast(immutable(ubyte[])) import("build.png"); 8 | static immutable imageRun = cast(immutable(ubyte[])) import("run.png"); 9 | static immutable imageTest = cast(immutable(ubyte[])) import("test.png"); 10 | static immutable imageCycle = cast(immutable(ubyte[])) import("cycle.png"); 11 | static immutable imageDlang = cast(immutable(ubyte[])) import("dlang.png"); 12 | static immutable imageDub = cast(immutable(ubyte[])) import("dub.png"); 13 | void addAllImage(UIContext context) 14 | { 15 | context.addImage("corner", imageCorner); 16 | context.addImage("dido", imageDido); 17 | context.addImage("build", imageBuild); 18 | context.addImage("test", imageTest); 19 | context.addImage("run", imageRun); 20 | context.addImage("cycle", imageCycle); 21 | context.addImage("dlang", imageDlang); 22 | context.addImage("dub", imageDub); 23 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dido is a specific text editor for the D language. 2 | 3 | ![alt tag](https://raw.github.com/p0nce/dido/master/screenshots/dido.jpg) 4 | 5 | Status: Design failure. Multiple problems were met. Better start anew. 6 | 7 | ## What's inside 8 | 9 | - Multiple cursors editing 10 | - dido contains a small GUI package built on top of SDL2 renderers 11 | - portable graphics, SDL 2.x is the only dependency: https://www.libsdl.org/ 12 | - minimal DUB integration 13 | 14 | ## Current limitations 15 | 16 | - currently not configurable 17 | - no file browser 18 | - Multiple views not supported 19 | - only UTF-8 files support (load and save) 20 | - command language is very incomplete 21 | - currently no syntax highlighting 22 | - see Issues for more limitations 23 | 24 | ## Inspiration 25 | 26 | - dido's internals were heavily inspired by kakoune, a much more advanced and general editor. 27 | https://github.com/mawww/kakoune 28 | 29 | - Sublime Text is an inspiration for various things like multiple cursors 30 | 31 | - Visual Studio is an inspiration for UI 32 | 33 | ## Compiling 34 | 35 | - Build with DUB (a 2.066+ front-end is required) 36 | - Use recent SDL 2.x binaries 37 | 38 | -------------------------------------------------------------------------------- /UNLICENSE.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /source/dido/colors.d: -------------------------------------------------------------------------------- 1 | module dido.colors; 2 | 3 | public import std.random; 4 | 5 | import std.algorithm, 6 | std.math; 7 | 8 | public import gfm.math.vector, 9 | gfm.math.funcs, 10 | gfm.math.simplerng; 11 | 12 | 13 | vec3ub rgb(int r, int g, int b) pure nothrow 14 | { 15 | return vec3ub(cast(ubyte)r, cast(ubyte)g, cast(ubyte)b); 16 | } 17 | 18 | vec4ub rgba(int r, int g, int b, int a) pure nothrow 19 | { 20 | return vec4ub(cast(ubyte)r, cast(ubyte)g, cast(ubyte)b, cast(ubyte)a); 21 | } 22 | 23 | vec3ub lerpColor(vec3ub a, vec3ub b, float t) pure nothrow 24 | { 25 | vec3f af = cast(vec3f)a; 26 | vec3f bf = cast(vec3f)b; 27 | vec3f of = af * (1 - t) + bf * t; 28 | return cast(vec3ub)(0.5f + of); 29 | } 30 | 31 | vec4ub lerpColor(vec4ub a, vec4ub b, float t) pure nothrow 32 | { 33 | vec4f af = cast(vec4f)a; 34 | vec4f bf = cast(vec4f)b; 35 | vec4f of = af * (1 - t) + bf * t; 36 | return cast(vec4ub)(0.5f + of); 37 | } 38 | 39 | vec3ub mulColor(vec3ub color, float amount) pure nothrow 40 | { 41 | vec3f fcolor = cast(vec3f)color / 255.0f; 42 | fcolor *= amount; 43 | return cast(vec3ub)(0.5f + fcolor * 255.0f); 44 | } 45 | 46 | vec4ub mulColor(vec4ub color, float amount) pure nothrow 47 | { 48 | return vec4ub(mulColor(color.xyz, amount), color.w); 49 | } 50 | 51 | -------------------------------------------------------------------------------- /source/dido/window.d: -------------------------------------------------------------------------------- 1 | module dido.window; 2 | 3 | import gfm.sdl2; 4 | import dido.gui.font; 5 | 6 | final class Window 7 | { 8 | public: 9 | this(SDL2 sdl2) 10 | { 11 | 12 | int initialWidth = 800; 13 | int initialHeight = 700; 14 | 15 | _window = new SDL2Window(sdl2, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, initialWidth, initialHeight, 16 | SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); 17 | 18 | _window.setTitle("Dido v0.0.1"); 19 | 20 | // Try creating default renderer 21 | 22 | _renderer = new SDL2Renderer(_window, SDL_RENDERER_SOFTWARE); 23 | _renderer.setBlend(SDL_BLENDMODE_BLEND); 24 | 25 | _initialized = true; 26 | } 27 | 28 | ~this() 29 | { 30 | if (_initialized) 31 | { 32 | _renderer.destroy(); 33 | _window.destroy(); 34 | _initialized = false; 35 | } 36 | } 37 | 38 | void toggleFullscreen() 39 | { 40 | isFullscreen = !isFullscreen; 41 | if (isFullscreen) 42 | _window.setFullscreenSetting(SDL_WINDOW_FULLSCREEN_DESKTOP); 43 | else 44 | _window.setFullscreenSetting(0); 45 | } 46 | 47 | SDL2Renderer renderer() 48 | { 49 | return _renderer; 50 | } 51 | 52 | int getWidth() 53 | { 54 | return _window.getWidth(); 55 | } 56 | 57 | int getHeight() 58 | { 59 | return _window.getHeight(); 60 | } 61 | 62 | private: 63 | SDL2Window _window; 64 | SDL2Renderer _renderer; 65 | bool isFullscreen = false; 66 | bool _initialized; 67 | } -------------------------------------------------------------------------------- /source/dido/buffer/buffercommand.d: -------------------------------------------------------------------------------- 1 | module dido.buffer.buffercommand; 2 | 3 | import dido.buffer.selection; 4 | 5 | enum BufferCommandType 6 | { 7 | CHANGE_CHARS, // edit one text area 8 | BARRIER, // anchors for undo/redo, saves all selections location 9 | SAVE_SELECTIONS 10 | 11 | } 12 | 13 | struct ChangeCharsCommand 14 | { 15 | Selection oldSel; 16 | Selection newSel; 17 | dstring oldContent; // content of selection before applying this BufferCommand 18 | dstring newContent; // content of selection after applying this BufferCommand 19 | } 20 | 21 | struct BarrierCommand 22 | { 23 | } 24 | 25 | struct SaveSelectionsCommand 26 | { 27 | Selection[] selections; 28 | } 29 | 30 | struct BufferCommand 31 | { 32 | BufferCommandType type; 33 | union 34 | { 35 | ChangeCharsCommand changeChars; 36 | BarrierCommand barrier; 37 | SaveSelectionsCommand saveSelections; 38 | } 39 | } 40 | 41 | BufferCommand barrierCommand() 42 | { 43 | BufferCommand command; 44 | command.type = BufferCommandType.BARRIER; 45 | return command; 46 | } 47 | 48 | BufferCommand saveSelectionsCommand(Selection[] selectionToSave) 49 | { 50 | BufferCommand command; 51 | command.type = BufferCommandType.SAVE_SELECTIONS; 52 | command.saveSelections.selections = selectionToSave.dup; 53 | return command; 54 | } 55 | 56 | BufferCommand changeCharsCommand(Selection oldSel, 57 | Selection newSel, 58 | dstring oldContent, 59 | dstring newContent) 60 | { 61 | BufferCommand command; 62 | command.type = BufferCommandType.CHANGE_CHARS; 63 | command.changeChars = ChangeCharsCommand(oldSel, newSel, oldContent.dup, newContent.dup); 64 | return command; 65 | } 66 | -------------------------------------------------------------------------------- /source/dido/panel/solutionpanel.d: -------------------------------------------------------------------------------- 1 | module dido.panel.solutionpanel; 2 | 3 | import std.path : baseName; 4 | 5 | import dido.project; 6 | import dido.buffer.buffer; 7 | import dido.gui; 8 | 9 | class SolutionPanel : UIElement 10 | { 11 | public: 12 | 13 | this(UIContext context, Project[] projects) 14 | { 15 | super(context); 16 | _cameraY = 0; 17 | _projects = projects; 18 | } 19 | 20 | override void reflow(box2i availableSpace) 21 | { 22 | _position = availableSpace; 23 | } 24 | 25 | override void preRender(SDL2Renderer renderer) 26 | { 27 | 28 | int itemSpace = font.charHeight() + 12; 29 | int marginX = 16; 30 | int marginY = 16; 31 | 32 | foreach(int iproject, project; _projects) 33 | { 34 | int px = marginX; 35 | int py = marginY; 36 | //int width = 37 | //int height = iproject; 38 | 39 | 40 | } 41 | 42 | /* for(int i = 0; i < cast(int)_buffers.length; ++i) 43 | { 44 | renderer.setColor(25, 25, 25, 255); 45 | int rectMargin = 4; 46 | renderer.fillRect(marginX - rectMargin, marginY - rectMargin + i * itemSpace, _position.width - 2 * (marginX - rectMargin), itemSpace - 4); 47 | } 48 | 49 | for(int i = 0; i < cast(int)_buffers.length; ++i) 50 | { 51 | if (i == _bufferSelect) 52 | font.setColor(255, 255, 255, 255); 53 | else 54 | font.setColor(200, 200, 200, 255); 55 | font.renderString(_prettyName[i], marginX, marginY + i * itemSpace); 56 | } 57 | */ 58 | } 59 | 60 | void updateState(int projectSelect) 61 | { 62 | /*_items. 63 | 64 | _projects = projects;*/ 65 | _projectSelect = projectSelect; 66 | } 67 | 68 | 69 | private: 70 | string[] _prettyName; 71 | Project[] _projects; 72 | int _projectSelect; 73 | int _cameraY; 74 | 75 | ProjectItemPanel[] _items; 76 | } 77 | 78 | class ProjectItemPanel : UIElement 79 | { 80 | Project _project; 81 | 82 | this(UIContext context, Project project) 83 | { 84 | super(context); 85 | _project = project; 86 | } 87 | } -------------------------------------------------------------------------------- /source/dido/panel/westpanel.d: -------------------------------------------------------------------------------- 1 | module dido.panel.westpanel; 2 | 3 | import std.path : baseName; 4 | 5 | import dido.buffer.buffer; 6 | import dido.gui; 7 | import dido.engine; 8 | 9 | import dido.panel.solutionpanel, 10 | dido.panel.textarea, 11 | dido.panel.outputpanel; 12 | 13 | class WestPanel : UIElement 14 | { 15 | public: 16 | 17 | this(UIContext context, SolutionPanel solutionPanel, OutputPanel outputPanel) 18 | { 19 | super(context); 20 | addChild(solutionPanel); 21 | addChild(outputPanel); 22 | } 23 | 24 | override void reflow(box2i availableSpace) 25 | { 26 | _position = availableSpace; 27 | 28 | int margin = 2; 29 | 30 | box2i spaceForSolutionPanel = availableSpace; 31 | spaceForSolutionPanel.max.y = cast(int)(0.5 + spaceForSolutionPanel.min.y * 0.25 + 0.75 * spaceForSolutionPanel.max.y); 32 | box2i spaceForOutputPanel = availableSpace; 33 | spaceForOutputPanel.min.y = spaceForSolutionPanel.max.y; 34 | 35 | spaceForSolutionPanel.min.x += margin; 36 | spaceForSolutionPanel.max.x -= margin; 37 | spaceForSolutionPanel.min.y += margin; 38 | spaceForSolutionPanel.max.y -= margin / 2; 39 | 40 | spaceForOutputPanel.min.x += margin; 41 | spaceForOutputPanel.max.x -= margin; 42 | spaceForOutputPanel.min.y += margin / 2; 43 | 44 | child(0).reflow(spaceForSolutionPanel); 45 | child(1).reflow(spaceForOutputPanel); 46 | } 47 | 48 | override void preRender(SDL2Renderer renderer) 49 | { 50 | renderer.setColor(34, 34, 34, 255); 51 | renderer.fillRect(0, 0, _position.width, _position.height); 52 | } 53 | 54 | private: 55 | SolutionPanel _solutionPanel; 56 | OutputPanel _outputPanel; 57 | } 58 | 59 | class EastPanel : UIElement 60 | { 61 | public: 62 | this(UIContext context, DidoEngine engine, TextArea textArea) 63 | { 64 | _textArea = textArea; 65 | _engine = engine; 66 | super(context); 67 | addChild(textArea); 68 | } 69 | 70 | override bool onMousePreClick(int x, int y, int button, bool isDoubleClick) 71 | { 72 | _engine.leaveCommandLineMode(); 73 | return false; // let the event bubble anyway 74 | } 75 | 76 | private: 77 | TextArea _textArea; 78 | DidoEngine _engine; 79 | } -------------------------------------------------------------------------------- /source/dido/panel/outputpanel.d: -------------------------------------------------------------------------------- 1 | module dido.panel.outputpanel; 2 | 3 | import dido.gui; 4 | 5 | enum LineType 6 | { 7 | ERROR, 8 | SUCCESS, 9 | COMMAND, 10 | RESULT, 11 | EXTERNAL, 12 | } 13 | 14 | struct LineOutput 15 | { 16 | LineType type; 17 | dstring msg; 18 | } 19 | 20 | class OutputPanel : UIElement 21 | { 22 | public: 23 | 24 | enum textPadding = 4; 25 | 26 | this(UIContext context) 27 | { 28 | super(context); 29 | _maxHistory = 1000; 30 | } 31 | 32 | override void reflow(box2i availableSpace) 33 | { 34 | _position = availableSpace; 35 | } 36 | 37 | override void preRender(SDL2Renderer renderer) 38 | { 39 | renderer.setColor(8, 9, 10, 255); 40 | renderer.fillRect(0, 0, _position.width, _position.height); 41 | /* 42 | renderer.setColor(30, 30, 30, 255); 43 | renderer.drawRect(0, 0, _position.width, _position.height); 44 | */ 45 | 46 | int fh = font.charHeight(); 47 | 48 | // set camera automatically 49 | if (4 + _log.length * fh < _position.height) 50 | _cameraY = 0; 51 | else 52 | _cameraY = 4 + cast(int) _log.length * fh - _position.height; 53 | 54 | for (int i = 0; i < cast(int) (_log.length); ++i) 55 | { 56 | final switch(_log[i].type) with (LineType) 57 | { 58 | case ERROR: font.setColor(138, 36, 26); break; 59 | case SUCCESS: font.setColor(66, 137, 45); break; 60 | case EXTERNAL: font.setColor(128, 128, 128); break; 61 | case COMMAND: font.setColor(175, 176, 112); break; 62 | case RESULT: font.setColor(90, 168, 168); break; 63 | } 64 | int textX = textPadding; 65 | int textY = -_cameraY + textPadding + i * fh; 66 | font.renderString!dstring(_log[i].msg, textX, textY); 67 | } 68 | } 69 | 70 | void clearLog() 71 | { 72 | _log.length = 0; 73 | } 74 | 75 | void log(LineOutput lo) 76 | { 77 | _log ~= lo; 78 | if (_log.length > _maxHistory) 79 | { 80 | size_t toStrip = _log.length - _maxHistory; 81 | _log = _log[toStrip..$]; 82 | } 83 | 84 | } 85 | 86 | private: 87 | size_t _maxHistory; 88 | LineOutput[] _log; 89 | int _cameraY; 90 | } 91 | -------------------------------------------------------------------------------- /source/dido/gui/context.d: -------------------------------------------------------------------------------- 1 | module dido.gui.context; 2 | 3 | import core.stdc.stdlib; 4 | public import gfm.math; 5 | public import gfm.sdl2; 6 | import dido.pngload; 7 | public import dido.gui.font; 8 | import std.file; 9 | 10 | import dido.gui.element; 11 | 12 | class UIContext 13 | { 14 | public: 15 | this(SDL2 sdl2_, SDL2Renderer renderer_, Font font_) 16 | { 17 | renderer = renderer_; 18 | font = font_; 19 | 20 | 21 | sdl2 = sdl2_; 22 | } 23 | 24 | ~this() 25 | { 26 | foreach(t; _textures) 27 | t.destroy(); 28 | 29 | foreach(s; _surfaces) 30 | s.destroy(); 31 | } 32 | 33 | void addImage(string name, immutable(ubyte[]) data) 34 | { 35 | _surfaces[name] = loadImage(sdl2, data); 36 | auto texture = new SDL2Texture(renderer, _surfaces[name]); 37 | texture.setAlphaMod(255); 38 | texture.setColorMod(255, 255, 255); 39 | _textures[name] = texture; 40 | } 41 | 42 | SDL2 sdl2; 43 | SDL2Renderer renderer; 44 | Font font; 45 | UIElement dragged = null; // current dragged element 46 | 47 | SDL2Texture image(string name) 48 | { 49 | return _textures[name]; 50 | } 51 | 52 | void beginDragging(UIElement element) 53 | { 54 | stopDragging(); 55 | 56 | // Uncomment this once SDL_CaptureMouse is in Derelict 57 | // SDL_CaptureMouse(SDL_TRUE); 58 | 59 | dragged = element; 60 | dragged.onBeginDrag(); 61 | } 62 | 63 | void stopDragging() 64 | { 65 | if (dragged !is null) 66 | { 67 | dragged.onStopDrag(); 68 | dragged = null; 69 | 70 | // Uncomment this once SDL_CaptureMouse is in Derelict 71 | // SDL_CaptureMouse(SDL_FALSE); 72 | } 73 | } 74 | 75 | private: 76 | SDL2Surface[string] _surfaces; 77 | SDL2Texture[string] _textures; 78 | 79 | SDL2Surface loadImage(SDL2 sdl2, immutable(ubyte[]) imageData) 80 | { 81 | void[] data = cast(void[])imageData; 82 | int width, height, components; 83 | 84 | ubyte* decoded = stbi_load_png_from_memory(data, width, height, components, 4); 85 | scope(exit) free(decoded); 86 | 87 | // stb_image guarantees that ouput will always have 4 components when asked 88 | SDL2Surface loaded = new SDL2Surface(sdl2, decoded, width, height, 32, 4 * width, 89 | 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000); 90 | 91 | SDL2Surface cloned = loaded.clone(); // to gain pixel ownership 92 | loaded.destroy(); 93 | return cloned; 94 | } 95 | } 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /source/dido/panel/commandlinepanel.d: -------------------------------------------------------------------------------- 1 | module dido.panel.commandlinepanel; 2 | 3 | import gfm.math; 4 | import dido.gui; 5 | import dido.colors; 6 | import dido.engine; 7 | 8 | import dido.panel.textarea; 9 | import dido.buffer.buffer; 10 | 11 | static immutable vec3i statusGreen = vec3i(0, 255, 0); 12 | static immutable vec3i statusYellow = vec3i(255, 255, 0); 13 | static immutable vec3i statusRed = vec3i(255, 0, 0); 14 | 15 | 16 | class CommandLinePanel : UIElement 17 | { 18 | public: 19 | 20 | int margin = 4; 21 | 22 | this(UIContext context) 23 | { 24 | super(context); 25 | _statusLine = ""; 26 | _buffer = new Buffer(); 27 | _textArea = new TextArea(context, margin, false, false, rgba(15, 14, 14, 0)); 28 | addChild(_textArea); 29 | } 30 | 31 | override void reflow(box2i availableSpace) 32 | { 33 | _position = availableSpace; 34 | _position.min.y = availableSpace.max.y - (2 * margin + charHeight); 35 | 36 | availableSpace = _position; 37 | availableSpace.min.x += charWidth; 38 | _textArea.reflow(availableSpace); 39 | } 40 | 41 | override void preRender(SDL2Renderer renderer) 42 | { 43 | renderer.setColor(15, 14, 14, 255); 44 | renderer.fillRect(0, 0, _position.width, _position.height); 45 | 46 | { 47 | // commandline bar at bottom 48 | 49 | int textPosx = 4 + charWidth; 50 | int textPosy = 4; 51 | 52 | if (_commandLineMode) 53 | { 54 | font.setColor(255, 255, 0, 255); 55 | font.renderString(":", 4, 4); 56 | } 57 | else 58 | { 59 | // Write status line 60 | font.setColor(_statusColor.r, _statusColor.g, _statusColor.b, 255); 61 | font.renderString(_statusLine, textPosx, textPosy); 62 | } 63 | } 64 | } 65 | 66 | void updateMode(bool commandLineMode) 67 | { 68 | _commandLineMode = commandLineMode; 69 | _textArea.setVisible(commandLineMode); 70 | } 71 | 72 | void updateCursorState(bool showCursors) 73 | { 74 | _textArea.setState(_buffer, showCursors); 75 | } 76 | 77 | dstring getCommandLine() 78 | { 79 | return _buffer.getContent(); 80 | } 81 | 82 | Buffer buffer() 83 | { 84 | return _buffer; 85 | } 86 | 87 | TextArea textArea() 88 | { 89 | return _textArea; 90 | } 91 | 92 | void updateEngine(DidoEngine engine) 93 | { 94 | _engine = engine; 95 | } 96 | 97 | override bool onMousePreClick(int x, int y, int button, bool isDoubleClick) 98 | { 99 | _engine.enterCommandLineMode(); 100 | return false; // let the event bubble anyway 101 | } 102 | 103 | 104 | private: 105 | bool _commandLineMode; 106 | TextArea _textArea; 107 | Buffer _buffer; 108 | DidoEngine _engine; 109 | 110 | dstring _statusLine; 111 | vec3i _statusColor; 112 | } 113 | -------------------------------------------------------------------------------- /source/main.d: -------------------------------------------------------------------------------- 1 | module app; 2 | 3 | import std.algorithm; 4 | import std.file; 5 | import std.array; 6 | import std.typecons; 7 | import std.stdio; 8 | import std.string; 9 | import std.path; 10 | 11 | import dido.app; 12 | import dido.config; 13 | 14 | void usage() 15 | { 16 | writefln("Dido: a text editor for D projects."); 17 | writefln("usage: dido "); 18 | writefln(""); 19 | writefln("-h / --help: show this help"); 20 | writefln(": add this DUB project"); 21 | } 22 | 23 | void main(string[] args) 24 | { 25 | try 26 | { 27 | 28 | string[] dubFiles; 29 | bool showHelp = false; 30 | 31 | // parse arguments 32 | for (int i = 1; i < cast(int)(args.length); ++i) 33 | { 34 | string arg = args[i]; 35 | if (arg == "-h" || arg == "--help") 36 | { 37 | showHelp = true; 38 | } 39 | else 40 | { 41 | dubFiles ~= args[i]; 42 | } 43 | } 44 | 45 | if (showHelp) 46 | { 47 | usage(); 48 | return; 49 | } 50 | 51 | if (dubFiles.length == 0) 52 | { 53 | dubFiles ~= "."; 54 | } 55 | 56 | // make all paths absolute 57 | for (int i = 0; i < cast(int)(dubFiles.length); ++i) 58 | { 59 | dubFiles[i] = buildNormalizedPath(absolutePath(dubFiles[i])); 60 | } 61 | 62 | // if given directories, find DUB files recursively 63 | for (int i = 0; i < cast(int)(dubFiles.length); ++i) 64 | { 65 | if (std.file.isDir(dubFiles[i])) 66 | { 67 | auto newFiles = filter!`endsWith(a.name, "dub.json") || endsWith(a.name, "package.json")`(dirEntries(dubFiles[i], SpanMode.shallow)) 68 | .map!`a.name`.map!`std.path.absolutePath(a)`; 69 | dubFiles = dubFiles ~ newFiles.array; 70 | } 71 | 72 | } 73 | 74 | // remove all directories and duplicates 75 | dubFiles = filter!`!std.file.isDir(a)`(dubFiles).uniq.array; 76 | 77 | // check that there is anything left 78 | if (dubFiles.length == 0) 79 | { 80 | throw new Exception("Didn't found any package.json or dub.json"); 81 | } 82 | 83 | // check file names and that they exist 84 | for (int i = 0; i < cast(int)(dubFiles.length); ++i) 85 | { 86 | string s = baseName(dubFiles[i]); 87 | if (s != "dub.json" && s != "package.json") 88 | throw new Exception(format("File '%s' should be named package.json or dub.json")); 89 | 90 | if (!exists(dubFiles[i])) 91 | throw new Exception(format("File '%s' does not exist")); 92 | } 93 | 94 | DidoConfig config = new DidoConfig; 95 | 96 | auto app = scoped!App(config, dubFiles); 97 | app.mainLoop(); 98 | 99 | } 100 | catch(Exception e) 101 | { 102 | writefln("error: %s", e.msg); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /source/dido/gui/button.d: -------------------------------------------------------------------------------- 1 | module dido.gui.button; 2 | 3 | import std.algorithm; 4 | 5 | import gfm.math; 6 | import dido.gui; 7 | 8 | class UIButton : UIElement 9 | { 10 | public: 11 | 12 | this(UIContext context, dstring label, string icon = null) 13 | { 14 | super(context); 15 | _label = label; 16 | 17 | _paddingW = 8; 18 | _paddingH = 4; 19 | _icon = icon; 20 | _iconWidth = 0; 21 | _iconHeight = 0; 22 | if (_icon !is null) 23 | { 24 | _iconImage = context.image(icon); 25 | _iconWidth = _iconImage.width(); 26 | _iconHeight = _iconImage.height(); 27 | } 28 | } 29 | 30 | enum marginIcon = 6; 31 | 32 | override void reflow(box2i availableSpace) 33 | { 34 | int width = 2 * _paddingW + cast(int) _label.length * font.charWidth; 35 | if (_icon !is null) 36 | width += marginIcon + _iconWidth; 37 | int height = 2 * _paddingH + font.charHeight; 38 | _position = box2i(availableSpace.min.x, availableSpace.min.y, availableSpace.min.x + width, availableSpace.min.y + height); 39 | } 40 | 41 | override void preRender(SDL2Renderer renderer) 42 | { 43 | if (isMouseOver()) 44 | { 45 | renderer.setColor(30, 27, 27, 255); 46 | renderer.fillRect(1, 1, _position.width - 2, _position.height -2); 47 | } 48 | 49 | if (isMouseOver()) 50 | renderer.setColor(70, 67, 67, 255); 51 | else 52 | renderer.setColor(30, 27, 27, 255); 53 | 54 | renderer.drawRect(0, 0, _position.width, _position.height); 55 | 56 | if (isMouseOver()) 57 | font.setColor(255, 255, 200); 58 | else 59 | font.setColor(200, 200, 200); 60 | 61 | dstring textChoice = _label; 62 | int heightOfText = font.charHeight; 63 | int widthOfTextPlusIcon = font.charWidth * cast(int) textChoice.length; 64 | if (_icon !is null) 65 | widthOfTextPlusIcon += marginIcon + _iconWidth; 66 | 67 | int iconX = 1 + (position.width - widthOfTextPlusIcon) / 2; 68 | int textX = iconX; 69 | if (_icon !is null) 70 | { 71 | textX += marginIcon + _iconWidth; 72 | renderer.copy(_iconImage, iconX, 1 + (position.height - _iconHeight) / 2); 73 | } 74 | font.renderString(textChoice, textX, 1 + (position.height - heightOfText) / 2); 75 | } 76 | 77 | private: 78 | dstring _label; 79 | int _paddingW; 80 | int _paddingH; 81 | string _icon; 82 | int _iconWidth; 83 | int _iconHeight; 84 | SDL2Texture _iconImage; 85 | } 86 | 87 | class UIImage : UIElement 88 | { 89 | public: 90 | this(UIContext context, string imageName) 91 | { 92 | super(context); 93 | _tex = context.image(imageName); 94 | _width = _tex.width(); 95 | _height = _tex.height(); 96 | } 97 | 98 | override void preRender(SDL2Renderer renderer) 99 | { 100 | renderer.copy(_tex, 0, 0); 101 | } 102 | 103 | override void reflow(box2i availableSpace) 104 | { 105 | _position = availableSpace; 106 | 107 | // TODO leave this logic out of UIImage 108 | _position.min.x = _position.max.x - _width; 109 | _position.min.y = _position.max.y - _height; 110 | } 111 | 112 | private: 113 | SDL2Texture _tex; 114 | int _width; 115 | int _height; 116 | } -------------------------------------------------------------------------------- /source/dido/project.d: -------------------------------------------------------------------------------- 1 | module dido.project; 2 | 3 | import std.path; 4 | import std.string; 5 | import std.file; 6 | import std.json; 7 | import std.process; 8 | 9 | import dido.buffer.buffer; 10 | 11 | // Model 12 | class Project 13 | { 14 | private: 15 | string _absPath; // path of dub.json 16 | 17 | string _mainPackage; 18 | 19 | Buffer[] _buffers; 20 | int _bufferSelect; 21 | 22 | ProjectPackage[] _packages; 23 | 24 | public: 25 | this(string absPath) 26 | { 27 | _absPath = absPath; 28 | getDubDescription(); 29 | 30 | foreach (ProjectPackage projectPackage; _packages) 31 | { 32 | foreach (ref path; projectPackage.files) 33 | { 34 | Buffer buf = new Buffer(path); 35 | _buffers ~= buf; 36 | } 37 | } 38 | 39 | // create an empty buffer if no file provided 40 | if (_buffers.length == 0) 41 | { 42 | Buffer buf = new Buffer; 43 | _buffers ~= buf; 44 | } 45 | 46 | _bufferSelect = 0; 47 | } 48 | 49 | int numBuffers() 50 | { 51 | return cast(int)(_buffers.length); 52 | } 53 | 54 | // change current edited buffer, but not the project 55 | void setCurrentBuffer(int bufferSelect) 56 | { 57 | _bufferSelect = bufferSelect; 58 | } 59 | 60 | void nextBuffer() 61 | { 62 | setCurrentBuffer( (_bufferSelect + 1) % cast(int) _buffers.length ); 63 | } 64 | 65 | void previousBuffer() 66 | { 67 | setCurrentBuffer( (_bufferSelect + cast(int) _buffers.length - 1) % cast(int) _buffers.length ); 68 | } 69 | 70 | int bufferSelect(int x) 71 | { 72 | return _bufferSelect = x; 73 | } 74 | 75 | int bufferSelect() 76 | { 77 | return _bufferSelect; 78 | } 79 | 80 | ref Buffer[] buffers() 81 | { 82 | return _buffers; 83 | } 84 | 85 | Buffer currentBuffer() 86 | { 87 | return _buffers[_bufferSelect]; 88 | } 89 | 90 | private: 91 | 92 | void getDubDescription() 93 | { 94 | // change directory 95 | string oldDir = getcwd(); 96 | chdir(dirName(_absPath)); 97 | scope(exit) 98 | chdir(oldDir); 99 | 100 | auto dubResult = execute(["dub", "describe"]); 101 | 102 | if (dubResult.status != 0) 103 | throw new Exception(format("dub returned %s", dubResult.status)); 104 | 105 | JSONValue description = parseJSON(dubResult.output); 106 | 107 | try 108 | { 109 | _mainPackage = description["mainPackage"].str; 110 | } 111 | catch(Exception e) 112 | { 113 | _mainPackage = description["rootPackage"].str; 114 | } 115 | 116 | foreach (pack; description["packages"].array()) 117 | { 118 | ProjectPackage projectPackage = new ProjectPackage; 119 | 120 | projectPackage.name = pack["name"].str; 121 | projectPackage.absPath = pack["path"].str; 122 | 123 | foreach (file; pack["files"].array()) 124 | { 125 | string filepath = file["path"].str(); 126 | 127 | // only add files dido can render 128 | if (filepath.endsWith(".d") || filepath.endsWith(".json") || filepath.endsWith(".res")) 129 | { 130 | projectPackage.files ~= buildPath(projectPackage.absPath, filepath); 131 | } 132 | } 133 | 134 | _packages ~= projectPackage; 135 | } 136 | } 137 | } 138 | 139 | 140 | class ProjectPackage 141 | { 142 | string absPath; 143 | string name; 144 | string[] files; 145 | } -------------------------------------------------------------------------------- /source/dido/gui/combobox.d: -------------------------------------------------------------------------------- 1 | module dido.gui.combobox; 2 | 3 | import std.algorithm; 4 | 5 | import gfm.math; 6 | import dido.gui; 7 | 8 | class ComboBox : UIElement 9 | { 10 | public: 11 | 12 | this(UIContext context, dstring[] labels, dstring[] choices, string icon = null) 13 | { 14 | super(context); 15 | _labels = labels; 16 | _choices = choices; 17 | assert(_labels.length == _choices.length); 18 | _select = -1; 19 | 20 | _paddingW = 8; 21 | _paddingH = 4; 22 | setSelectedChoice(0); 23 | 24 | _icon = icon; 25 | _iconWidth = 0; 26 | _iconHeight = 0; 27 | if (_icon !is null) 28 | { 29 | _iconImage = context.image(icon); 30 | _iconWidth = _iconImage.width(); 31 | _iconHeight = _iconImage.height(); 32 | } 33 | } 34 | 35 | enum marginIcon = 6; 36 | 37 | final void setSelectedChoice(int n) 38 | { 39 | if (_select != n) 40 | { 41 | _select = n; 42 | onChoice(n); 43 | } 44 | } 45 | 46 | // Called whenever a combobox is selected. 47 | void onChoice(int n) 48 | { 49 | // do nothing 50 | } 51 | 52 | override void reflow(box2i availableSpace) 53 | { 54 | int width = 2 * _paddingW + longestStringLength() * font.charWidth; 55 | if (_icon !is null) 56 | width += marginIcon + _iconWidth; 57 | int height = 2 * _paddingH + font.charHeight; 58 | 59 | _position = box2i(availableSpace.min.x, availableSpace.min.y, availableSpace.min.x + width, availableSpace.min.y + height); 60 | } 61 | 62 | override void preRender(SDL2Renderer renderer) 63 | { 64 | if (isMouseOver()) 65 | { 66 | renderer.setColor(30, 27, 27, 255); 67 | renderer.fillRect(1, 1, _position.width - 2, _position.height -2); 68 | } 69 | 70 | if (isMouseOver()) 71 | renderer.setColor(70, 67, 67, 255); 72 | else 73 | renderer.setColor(30, 27, 27, 255); 74 | 75 | renderer.drawRect(0, 0, _position.width, _position.height); 76 | 77 | if (isMouseOver()) 78 | font.setColor(255, 255, 200); 79 | else 80 | font.setColor(200, 200, 200); 81 | 82 | dstring textChoice = label(_select); 83 | int heightOfText = font.charHeight; 84 | int widthOfText = font.charWidth * cast(int) textChoice.length; 85 | int iconX = _paddingW; 86 | int availableWidthForText = position.width - ((_icon is null) ? 0 : (marginIcon + _iconWidth)); 87 | int textX = 1 + (availableWidthForText - widthOfText) / 2; 88 | if (_icon !is null) 89 | { 90 | textX += marginIcon + _iconWidth; 91 | renderer.copy(_iconImage, iconX, 1 + (position.height - _iconHeight) / 2); 92 | } 93 | font.renderString(textChoice, textX, 1 + (position.height - heightOfText) / 2); 94 | } 95 | 96 | override bool onMousePostClick(int x, int y, int button, bool isDoubleClick) 97 | { 98 | if (_choices.length == 0) 99 | return false; 100 | setSelectedChoice((_select + 1) % cast(int) _choices.length); 101 | return true; 102 | } 103 | 104 | // Called when mouse move over this Element. 105 | override void onMouseMove(int x, int y, int dx, int dy) 106 | { 107 | } 108 | 109 | // Called when mouse enter this Element. 110 | override void onMouseExit() 111 | { 112 | } 113 | 114 | dstring choice(int n) 115 | { 116 | return _choices[n]; 117 | } 118 | 119 | dstring label(int n) 120 | { 121 | return _labels[n]; 122 | } 123 | 124 | private: 125 | 126 | int _select; 127 | int _paddingW; 128 | int _paddingH; 129 | 130 | string _icon; 131 | int _iconWidth; 132 | int _iconHeight; 133 | SDL2Texture _iconImage; 134 | 135 | dstring[] _labels; 136 | dstring[] _choices; 137 | 138 | int longestStringLength() 139 | { 140 | int maximum = 0; 141 | foreach(ref dstring c; _labels) 142 | { 143 | if (maximum < cast(int) c.length) 144 | maximum = cast(int) c.length; 145 | } 146 | return maximum; 147 | } 148 | } -------------------------------------------------------------------------------- /fonts/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | 5 | This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /source/dido/builder.d: -------------------------------------------------------------------------------- 1 | module dido.builder; 2 | 3 | import core.thread; 4 | 5 | import std.array; 6 | import std.conv; 7 | import std.string; 8 | import std.process; 9 | 10 | 11 | import dido.engine; 12 | import dido.panel.outputpanel; 13 | 14 | class Builder 15 | { 16 | public: 17 | this(DidoEngine engine) 18 | { 19 | _engine = engine; 20 | } 21 | 22 | void startBuild(string compiler, string arch, string build) 23 | { 24 | stopBuild(); 25 | if (_state != State.initial) 26 | return; 27 | 28 | _state = State.building; 29 | _buildThread = new BuildThread(this, BuildAction.build, compiler, arch, build); 30 | _buildThread.start(); 31 | } 32 | 33 | void startRun(string compiler, string arch, string build) 34 | { 35 | stopBuild(); 36 | if (_state != State.initial) 37 | return; 38 | 39 | _state = State.running; 40 | _buildThread = new BuildThread(this, BuildAction.run, compiler, arch, build); 41 | _buildThread.start(); 42 | } 43 | 44 | void startTest(string compiler, string arch, string build) 45 | { 46 | stopBuild(); 47 | if (_state != State.initial) 48 | return; 49 | 50 | _state = State.building; 51 | _buildThread = new BuildThread(this, BuildAction.test, compiler, arch, build); 52 | _buildThread.start(); 53 | } 54 | 55 | void stopBuild() 56 | { 57 | if (_state == State.initial) 58 | return; 59 | 60 | _state = State.initial; 61 | _buildThread.signalStop(); 62 | _buildThread.join(); 63 | } 64 | 65 | private: 66 | enum State 67 | { 68 | initial, 69 | building, 70 | running 71 | } 72 | shared State _state = State.initial; 73 | 74 | DidoEngine _engine; 75 | 76 | BuildThread _buildThread; 77 | } 78 | 79 | enum BuildAction 80 | { 81 | build, 82 | test, 83 | run 84 | } 85 | 86 | class BuildThread : Thread 87 | { 88 | this(Builder builder, BuildAction action, string compiler, string arch, string build) 89 | { 90 | _action = action; 91 | _builder = builder; 92 | 93 | _compiler = compiler; 94 | _arch = arch; 95 | _build = build; 96 | super( &run ); 97 | } 98 | 99 | void signalStop() 100 | { 101 | synchronized(this) 102 | _stop = true; 103 | } 104 | 105 | private: 106 | Builder _builder; 107 | BuildAction _action; 108 | bool _stop = false; 109 | string _compiler; 110 | string _arch; 111 | string _build; 112 | 113 | void run() 114 | { 115 | string command; 116 | final switch(_action) with (BuildAction) 117 | { 118 | case BuildAction.run: command = "run"; break; 119 | case BuildAction.build: command = "build"; break; 120 | case BuildAction.test: command = "test"; break; 121 | } 122 | 123 | auto commands = ["dub", command, "--compiler", _compiler, "--arch", _arch, "--build", _build]; 124 | string asOneLine = "$" ~ std.array.join(commands, " "); 125 | 126 | _builder._engine.logMessage(LineType.COMMAND, to!dstring(asOneLine)); 127 | 128 | 129 | auto pipes = pipeProcess(["dub", command], Redirect.stdout | Redirect.stderr); 130 | scope(exit) 131 | { 132 | int exitCode = wait(pipes.pid); 133 | if (exitCode == 0) 134 | _builder._engine.logMessage(LineType.RESULT, "Done."d); 135 | else 136 | { 137 | dstring msg = to!dstring(format("dub returned %s", exitCode)); 138 | _builder._engine.logMessage(LineType.ERROR, msg); 139 | } 140 | } 141 | 142 | // pipe stdout to output window 143 | foreach (line; pipes.stdout.byLine) 144 | { 145 | synchronized // polling exit condition 146 | { 147 | if (_stop) 148 | { 149 | _builder._engine.greenMessage("Build was interrupted."); 150 | return; 151 | } 152 | } 153 | _builder._engine.logMessage(LineType.EXTERNAL, to!dstring(line)); 154 | } 155 | 156 | // pipe stderr to output window 157 | foreach (line; pipes.stderr.byLine) 158 | { 159 | synchronized // polling exit condition 160 | { 161 | if (_stop) 162 | { 163 | _builder._engine.greenMessage("Build was interrupted."); 164 | return; 165 | } 166 | } 167 | _builder._engine.logMessage(LineType.EXTERNAL, to!dstring(line)); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /source/dido/buffer/bufferiterator.d: -------------------------------------------------------------------------------- 1 | module dido.buffer.bufferiterator; 2 | 3 | import dido.buffer.buffer; 4 | import dido.buffer.selection; 5 | 6 | 7 | struct Cursor 8 | { 9 | public: 10 | int line = 0; 11 | int column = 0; 12 | 13 | int opCmp(ref const(Cursor) other) pure const nothrow 14 | { 15 | if (line != other.line) 16 | return line - other.line; 17 | if (column != other.column) 18 | return column - other.column; 19 | return 0; 20 | } 21 | } 22 | 23 | 24 | struct BufferIterator 25 | { 26 | private: 27 | Buffer _buffer = null; 28 | Cursor _cursor = Cursor(0, 0); 29 | public: 30 | 31 | this(Buffer buffer, Cursor cursor) 32 | { 33 | _buffer = buffer; 34 | _cursor = cursor; 35 | assert(_buffer !is null); 36 | } 37 | 38 | ref inout(Cursor) cursor() pure inout nothrow 39 | { 40 | return _cursor; 41 | } 42 | 43 | inout(Buffer) buffer() pure inout nothrow 44 | { 45 | return _buffer; 46 | } 47 | 48 | bool opEquals(ref const(BufferIterator) other) pure const nothrow 49 | { 50 | assert(_buffer is other._buffer); 51 | return _cursor == other._cursor; 52 | } 53 | 54 | bool canBeDecremented() 55 | { 56 | BufferIterator begin = _buffer.begin(); 57 | return this > begin; 58 | } 59 | 60 | bool canBeIncremented() 61 | { 62 | BufferIterator end = _buffer.end(); 63 | return this < end; 64 | } 65 | 66 | bool canGoInDirection(int dx) 67 | { 68 | assert(dx == -1 || dx == 1); 69 | if (dx == 1) 70 | return canBeIncremented(); 71 | else 72 | return canBeDecremented(); 73 | } 74 | 75 | BufferIterator opUnary(string op)() if (op == "++") 76 | { 77 | _cursor.column += 1; 78 | if (_cursor.column >= _buffer.lineLength(cursor.line)) 79 | { 80 | if (_cursor.line + 1 < _buffer.numLines()) 81 | { 82 | _cursor.line++; 83 | _cursor.column = 0; 84 | } 85 | else 86 | { 87 | _cursor.column = _buffer.lineLength(_buffer.numLines() - 1); 88 | assert(_cursor.column > 0); 89 | } 90 | } 91 | assert(isValid()); 92 | return this; 93 | } 94 | 95 | BufferIterator opUnary(string op)() if (op == "--") 96 | { 97 | assert(_buffer !is null); 98 | _cursor.column -= 1; 99 | if (_cursor.column < 0) 100 | { 101 | if (_cursor.line > 0) 102 | { 103 | _cursor.line--; 104 | _cursor.column = _buffer.lineLength(cursor.line) - 1; 105 | 106 | if (_cursor.column < 0) 107 | _cursor.column = 0; 108 | } 109 | else 110 | { 111 | _cursor.column = 0; 112 | } 113 | } 114 | assert(isValid()); 115 | return this; 116 | } 117 | 118 | BufferIterator opOpAssign(string op)(int displacement) if (op == "+") 119 | { 120 | while (displacement > 0 && canBeIncremented()) 121 | { 122 | displacement--; 123 | opUnary!("++")(); 124 | } 125 | while (displacement < 0 && canBeDecremented()) 126 | { 127 | displacement++; 128 | opUnary!("--")(); 129 | } 130 | return this; 131 | } 132 | 133 | BufferIterator opOpAssign(string op)(int displacement) if (op == "-") 134 | { 135 | return opOpAssign!"+="(-displacement); 136 | } 137 | 138 | BufferIterator opBinary(string op)(int displacement) if (op == "+") 139 | { 140 | BufferIterator result = this; 141 | return result += displacement; 142 | } 143 | 144 | BufferIterator opBinary(op)(int displacement) if (op == "-") 145 | { 146 | BufferIterator result = this; 147 | result -= displacement; 148 | return result; 149 | } 150 | 151 | int opCmp(ref const(BufferIterator) other) pure const nothrow 152 | { 153 | return cursor.opCmp(other._cursor); 154 | } 155 | 156 | dchar read() pure inout nothrow 157 | { 158 | assert(isValid()); 159 | return buffer.line(cursor.line)[cursor.column]; 160 | } 161 | 162 | bool isValid() pure inout nothrow 163 | { 164 | if (cursor.line < 0) 165 | return false; 166 | if (cursor.line >= buffer.numLines()) 167 | return false; 168 | if (cursor.column < 0) 169 | return false; 170 | 171 | // On last line, it is legal to have the cursor at the end of the line 172 | int lineLength = buffer.lineLength(cursor.line); 173 | if (cursor.column >= lineLength) 174 | { 175 | if ( (cursor.line == buffer.numLines() - 1) && cursor.column == lineLength) 176 | return true; 177 | else 178 | return false; 179 | } 180 | return true; 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /source/dido/gui/scrollbar.d: -------------------------------------------------------------------------------- 1 | module dido.gui.scrollbar; 2 | 3 | import std.algorithm; 4 | 5 | import gfm.math; 6 | import dido.gui; 7 | 8 | class ScrollBar : UIElement 9 | { 10 | public: 11 | 12 | this(UIContext context, int thicknessOfFocusBar, int padding, bool vertical) 13 | { 14 | super(context); 15 | _vertical = vertical; 16 | _thicknessOfFocusBar = thicknessOfFocusBar; 17 | _padding = padding; 18 | setProgress(0.45f, 0.55f); 19 | } 20 | 21 | // Called whenever a scrollbar move is done. Override it to change behaviour. 22 | void onScrollChangeMouse(float newProgressStart) 23 | { 24 | // do nothing 25 | } 26 | 27 | int thickness() pure const nothrow 28 | { 29 | return _thicknessOfFocusBar + 2 * _padding; 30 | } 31 | 32 | override void reflow(box2i availableSpace) 33 | { 34 | _position = availableSpace; 35 | 36 | if (_vertical) 37 | { 38 | _position.min.x = _position.max.x - thickness(); 39 | } 40 | else 41 | { 42 | _position.min.y = _position.max.y - thickness(); 43 | } 44 | } 45 | 46 | override void preRender(SDL2Renderer renderer) 47 | { 48 | // Do not display useless scrollbar 49 | if (_progressStart <= 0.0f && _progressStop >= 1.0f) 50 | return; 51 | 52 | renderer.setColor(0x30, 0x2C, 0x2C, 255); 53 | renderer.fillRect(0, 0, _position.width, _position.height); 54 | 55 | if (isMouseOver()) 56 | renderer.setColor(120, 120, 120, 255); 57 | else 58 | renderer.setColor( 80, 80, 80, 255); 59 | 60 | box2i focus = getFocusBox(); 61 | roundedRect(renderer, focus); 62 | } 63 | 64 | void roundedRect(SDL2Renderer renderer, box2i b) 65 | { 66 | if (b.height > 2 && b.width > 2) 67 | { 68 | renderer.fillRect(b.min.x + 1, b.min.y , b.width - 2, 1); 69 | renderer.fillRect(b.min.x , b.min.y + 1, b.width , b.height - 2); 70 | renderer.fillRect(b.min.x + 1, b.max.y - 1, b.width - 2, 1); 71 | } 72 | else 73 | renderer.fillRect(b.min.x, b.min.y, b.width, b.height); 74 | } 75 | 76 | void setProgress(float progressStart, float progressStop) 77 | { 78 | _progressStart = gfm.math.clamp!float(progressStart, 0.0f, 1.0f); 79 | _progressStop = gfm.math.clamp!float(progressStop, 0.0f, 1.0f); 80 | if (_progressStop < _progressStart) 81 | _progressStop = _progressStart; 82 | } 83 | 84 | override bool onMousePostClick(int x, int y, int button, bool isDoubleClick) 85 | { 86 | float clickProgress; 87 | if (_vertical) 88 | { 89 | int heightWithoutButton = _position.height; 90 | clickProgress = cast(float)(y) / heightWithoutButton; 91 | } 92 | else 93 | { 94 | int widthWithoutButton = _position.width; 95 | clickProgress = cast(float)(x) / widthWithoutButton; 96 | } 97 | 98 | // now this clickProgress should move the _center_ of the scrollbar to it 99 | float newStartProgress = clickProgress - (_progressStop - _progressStart) * 0.5f; 100 | 101 | onScrollChangeMouse(newStartProgress); 102 | return true; 103 | } 104 | 105 | // Called when mouse move over this Element. 106 | override void onMouseDrag(int x, int y, int dx, int dy) 107 | { 108 | float clickProgress; 109 | if (_vertical) 110 | { 111 | int heightWithoutButton = _position.height; 112 | clickProgress = cast(float)(dy) / heightWithoutButton; 113 | } 114 | else 115 | { 116 | int widthWithoutButton = _position.width; 117 | clickProgress = cast(float)(dx) / widthWithoutButton; 118 | } 119 | onScrollChangeMouse(_progressStart + clickProgress); 120 | } 121 | 122 | private: 123 | 124 | int _thicknessOfFocusBar; 125 | int _padding; 126 | 127 | bool _vertical; 128 | float _progressStart; 129 | float _progressStop; 130 | 131 | box2i getFocusBox() 132 | { 133 | if (_vertical) 134 | { 135 | int iprogressStart = cast(int)(0.5f + _progressStart * (_position.height - 2 * _padding)); 136 | int iprogressStop = cast(int)(0.5f + _progressStop * (_position.height - 2 * _padding)); 137 | int x = _padding; 138 | int y = iprogressStart + _padding; 139 | return box2i(x, y, x + _position.width - 2 * _padding, y + iprogressStop - iprogressStart); 140 | } 141 | else 142 | { 143 | int iprogressStart = cast(int)(0.5f + _progressStart * (_position.width - 2 * _padding)); 144 | int iprogressStop = cast(int)(0.5f + _progressStop * (_position.width - 2 * _padding)); 145 | int x = iprogressStart + _padding; 146 | int y = _padding; 147 | return box2i(x, y, x + iprogressStop - iprogressStart, y + _position.height - 2 * _padding); 148 | } 149 | } 150 | 151 | } -------------------------------------------------------------------------------- /source/dido/panel/menupanel.d: -------------------------------------------------------------------------------- 1 | module dido.panel.menupanel; 2 | 3 | import std.conv; 4 | 5 | import dido.gui; 6 | import dido.app; 7 | import dido.engine; 8 | 9 | class MenuPanel : UIElement 10 | { 11 | this(UIContext context, DidoEngine engine) 12 | { 13 | super(context); 14 | 15 | addChild(new BuildButton(context, engine)); 16 | addChild(new RunButton(context, engine)); 17 | addChild(new TestButton(context, engine)); 18 | 19 | addChild(new CompilerCombo(context, engine)); 20 | addChild(new ArchCombo(context, engine)); 21 | addChild(new BuildCombo(context, engine)); 22 | addChild(new DlangOrgButton(context)); 23 | addChild(new DubRegistryButton(context)); 24 | } 25 | 26 | override void reflow(box2i availableSpace) 27 | { 28 | _position = availableSpace; 29 | _position.max.y = availableSpace.min.y + 8 + 8 + charHeight; 30 | 31 | int margin = 4; 32 | 33 | availableSpace = _position.shrink(margin); 34 | 35 | availableSpace.min.x += 16 + 6 + margin; 36 | 37 | void reflowChild(int n) 38 | { 39 | child(n).reflow(availableSpace); 40 | availableSpace.min.x = child(n).position.max.x + margin; 41 | } 42 | 43 | reflowChild(0); 44 | reflowChild(1); 45 | reflowChild(2); 46 | reflowChild(3); 47 | reflowChild(4); 48 | reflowChild(5); 49 | reflowChild(6); 50 | } 51 | 52 | override void preRender(SDL2Renderer renderer) 53 | { 54 | renderer.setColor(15, 14, 14, 255); 55 | renderer.fillRect(0, 0, _position.width, _position.height); 56 | 57 | renderer.copy(context.image("dido"), 6, (_position.height - 20) / 2); 58 | } 59 | } 60 | 61 | class BuildCombo : ComboBox 62 | { 63 | this(UIContext context, DidoEngine engine) 64 | { 65 | _engine = engine; 66 | super(context, 67 | [ "debug", "release", "nobounds" ], 68 | [ "debug", "release", "release-nobounds"], "cycle"); 69 | } 70 | 71 | override void onChoice(int n) 72 | { 73 | _engine.executeScheme("(define current-build \"" ~ to!string(choice(n)) ~ "\")"); 74 | } 75 | 76 | private: 77 | DidoEngine _engine; 78 | } 79 | 80 | class ArchCombo : ComboBox 81 | { 82 | this(UIContext context, DidoEngine engine) 83 | { 84 | _engine = engine; 85 | super(context, 86 | [ "32-bit", "64-bit" ], 87 | [ "x86", "x86_64" ], 88 | "cycle"); 89 | } 90 | 91 | override void onChoice(int n) 92 | { 93 | _engine.executeScheme("(define current-arch \"" ~ to!string(choice(n)) ~ "\")"); 94 | } 95 | private: 96 | DidoEngine _engine; 97 | } 98 | 99 | 100 | class CompilerCombo : ComboBox 101 | { 102 | this(UIContext context, DidoEngine engine) 103 | { 104 | _engine = engine; 105 | super(context, 106 | [ "DMD", "LDC" ], 107 | [ "DMD", "LDC" ], "cycle"); 108 | } 109 | 110 | override void onChoice(int n) 111 | { 112 | _engine.executeScheme("(define current-compiler \"" ~ to!string(choice(n)) ~ "\")"); 113 | } 114 | private: 115 | DidoEngine _engine; 116 | } 117 | 118 | class BuildButton : UIButton 119 | { 120 | public: 121 | this(UIContext context, DidoEngine engine) 122 | { 123 | super(context, "Build", "build"); 124 | _engine = engine; 125 | } 126 | 127 | override bool onMousePostClick(int x, int y, int button, bool isDoubleClick) 128 | { 129 | _engine.executeScheme("(build current-compiler current-arch current-build)"); 130 | return true; 131 | } 132 | private: 133 | DidoEngine _engine; 134 | } 135 | 136 | class TestButton : UIButton 137 | { 138 | public: 139 | this(UIContext context, DidoEngine engine) 140 | { 141 | super(context, "Test", "test"); 142 | _engine = engine; 143 | } 144 | 145 | override bool onMousePostClick(int x, int y, int button, bool isDoubleClick) 146 | { 147 | _engine.executeScheme("(build current-compiler current-arch current-build)"); 148 | return true; 149 | } 150 | private: 151 | DidoEngine _engine; 152 | } 153 | 154 | class RunButton : UIButton 155 | { 156 | public: 157 | this(UIContext context, DidoEngine engine) 158 | { 159 | super(context, "Run", "run"); 160 | _engine = engine; 161 | } 162 | 163 | override bool onMousePostClick(int x, int y, int button, bool isDoubleClick) 164 | { 165 | _engine.executeScheme("(run current-compiler current-arch current-build)"); 166 | return true; 167 | } 168 | 169 | private: 170 | DidoEngine _engine; 171 | } 172 | 173 | class DlangOrgButton : UIButton 174 | { 175 | public: 176 | this(UIContext context) 177 | { 178 | super(context, "Home", "dlang"); 179 | } 180 | 181 | override bool onMousePostClick(int x, int y, int button, bool isDoubleClick) 182 | { 183 | import std.process; 184 | browse("http://dlang.org/"); 185 | return true; 186 | } 187 | } 188 | 189 | class DubRegistryButton : UIButton 190 | { 191 | public: 192 | this(UIContext context) 193 | { 194 | super(context, "Registry", "dub"); 195 | } 196 | 197 | override bool onMousePostClick(int x, int y, int button, bool isDoubleClick) 198 | { 199 | import std.process; 200 | browse("http://code.dlang.org/"); 201 | return true; 202 | } 203 | } -------------------------------------------------------------------------------- /source/dido/gui/font.d: -------------------------------------------------------------------------------- 1 | module dido.gui.font; 2 | 3 | import std.file; 4 | import std.conv; 5 | import gfm.sdl2; 6 | import dido.pngload; 7 | import dido.stb_truetype; 8 | 9 | final class Font 10 | { 11 | public: 12 | this(SDL2 sdl2, SDL2Renderer renderer, string fontface, int ptSize) 13 | { 14 | _sdl2 = sdl2; 15 | _renderer = renderer; 16 | 17 | 18 | _fontData = cast(ubyte[])(std.file.read(fontface)); 19 | if (0 == stbtt_InitFont(&_font, _fontData.ptr, stbtt_GetFontOffsetForIndex(_fontData.ptr, 0))) 20 | throw new Exception("Coudln't load font " ~ fontface); 21 | 22 | _scaleFactor = stbtt_ScaleForPixelHeight(&_font, ptSize); 23 | 24 | stbtt_GetFontVMetrics(&_font, &_fontAscent, &_fontDescent, &_fontLineGap); 25 | 26 | int ax; 27 | stbtt_GetCodepointHMetrics(&_font, 'A', &ax, null); 28 | _charWidth = cast(int)(0.5 + (ax * _scaleFactor)); 29 | _charHeight = cast(int)(0.5 + (_fontAscent - _fontDescent + _fontLineGap) * _scaleFactor); 30 | 31 | _initialized = true; 32 | 33 | _r = 255; 34 | _g = 255; 35 | _b = 255; 36 | _a = 255; 37 | } 38 | 39 | ~this() 40 | { 41 | if (_initialized) 42 | { 43 | foreach (tex; _glyphCache) 44 | tex.destroy(); 45 | foreach (tex; _surfaceCache) 46 | tex.destroy(); 47 | _initialized = false; 48 | } 49 | } 50 | 51 | SDL2Texture getCharTexture(dchar ch) 52 | { 53 | if (ch == 0) 54 | ch = 0xFFFD; 55 | try 56 | { 57 | if (! (ch in _glyphCache)) 58 | { 59 | SDL2Texture tex; 60 | SDL2Surface surf; 61 | makeCharTexture(ch, tex, surf); 62 | _glyphCache[ch] = tex; 63 | _surfaceCache[ch] = surf; 64 | } 65 | 66 | return _glyphCache[ch]; 67 | } 68 | catch(SDL2Exception e) 69 | { 70 | if (ch == 0xFFFD) 71 | return null; 72 | 73 | // invalid glyph, return replacement character glyph 74 | return getCharTexture(0xFFFD); 75 | } 76 | } 77 | 78 | void makeCharTexture(dchar ch, out SDL2Texture texture, out SDL2Surface surface) 79 | { 80 | // Generate glyph coverage 81 | int width, height; 82 | int xoff, yoff; 83 | ubyte* glyphBitmap = stbtt_GetCodepointBitmap(&_font, _scaleFactor, _scaleFactor, ch , &width, &height, &xoff, &yoff); 84 | 85 | // Copy to a SDL surface 86 | uint Rmask = 0x00ff0000; 87 | uint Gmask = 0x0000ff00; 88 | uint Bmask = 0x000000ff; 89 | uint Amask = 0xff000000; 90 | 91 | surface = new SDL2Surface(_sdl2, _charWidth, _charHeight, 32, Rmask, Gmask, Bmask, Amask); 92 | { 93 | surface.lock(); 94 | scope(exit) surface.unlock(); 95 | 96 | // fill with transparent white 97 | for (int i = 0; i < _charHeight; ++i) 98 | { 99 | for (int j = 0; j < _charWidth; ++j) 100 | { 101 | ubyte* dest = &surface.pixels[i * surface.pitch + (j * 4)]; 102 | dest[0] = 255; 103 | dest[1] = 255; 104 | dest[2] = 255; 105 | dest[3] = 0; 106 | } 107 | } 108 | 109 | for (int i = 0; i < height; ++i) 110 | { 111 | for (int j = 0; j < width; ++j) 112 | { 113 | ubyte source = glyphBitmap[j + i * width]; 114 | int destX = j + xoff; 115 | int destY = i + yoff + cast(int)(0.5 + _fontAscent * _scaleFactor); 116 | 117 | if (destX >= 0 && destX < _charWidth) 118 | { 119 | if (destY >= 0 && destY < _charHeight) 120 | { 121 | ubyte* dest = &surface.pixels[destY * surface.pitch + (destX * 4)]; 122 | dest[3] = source; // fully white, but eventually transparent 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | // Free glyph coverage 130 | stbtt_FreeBitmap(glyphBitmap); 131 | 132 | texture = new SDL2Texture(_renderer, surface); 133 | } 134 | 135 | int charWidth() pure const nothrow 136 | { 137 | return _charWidth; 138 | } 139 | 140 | int charHeight() pure const nothrow 141 | { 142 | return _charHeight; 143 | } 144 | 145 | void setColor(int r, int g, int b, int a = 255) 146 | { 147 | _r = r; 148 | _g = g; 149 | _b = b; 150 | _a = a; 151 | } 152 | 153 | void renderString(StringType)(StringType s, int x, int y) 154 | { 155 | foreach(dchar ch; s) 156 | { 157 | SDL2Texture tex = getCharTexture(ch); 158 | tex.setColorMod(_r, _g, _b); 159 | tex.setAlphaMod(_a); 160 | _renderer.copy(tex, x, y); 161 | x += tex.width(); 162 | } 163 | } 164 | 165 | void renderChar(dchar ch, int x, int y) 166 | { 167 | SDL2Texture tex = getCharTexture(ch); 168 | tex.setColorMod(_r, _g, _b); 169 | tex.setAlphaMod(_a); 170 | _renderer.copy(tex, x, y); 171 | } 172 | 173 | 174 | private: 175 | 176 | int _r, _g, _b, _a; 177 | 178 | SDL2 _sdl2; 179 | 180 | SDL2Renderer _renderer; 181 | stbtt_fontinfo _font; 182 | ubyte[] _fontData; 183 | int _fontAscent, _fontDescent, _fontLineGap; 184 | 185 | SDL2Texture[dchar] _glyphCache; 186 | SDL2Surface[dchar] _surfaceCache; 187 | int _charWidth; 188 | int _charHeight; 189 | float _scaleFactor; 190 | bool _initialized; 191 | } 192 | -------------------------------------------------------------------------------- /source/dido/buffer/selection.d: -------------------------------------------------------------------------------- 1 | module dido.buffer.selection; 2 | 3 | import dido.buffer.buffer; 4 | import dido.buffer.bufferiterator; 5 | 6 | 7 | struct Selection 8 | { 9 | BufferIterator anchor; // selection start 10 | BufferIterator edge; // blinking cursor 11 | 12 | this(BufferIterator bothEnds) pure nothrow 13 | { 14 | anchor = bothEnds; 15 | edge = bothEnds; 16 | } 17 | 18 | this(BufferIterator anchor_, BufferIterator edge_) pure nothrow 19 | { 20 | edge = edge_; 21 | anchor = anchor_; 22 | assert(anchor.buffer is edge.buffer); 23 | } 24 | 25 | // start == stop => no selected area 26 | 27 | bool hasSelectedArea() pure const nothrow 28 | { 29 | return anchor != edge; 30 | } 31 | 32 | bool overlaps(Selection other) 33 | { 34 | Selection tsorted = this.sorted(); 35 | Selection osorted = other.sorted(); 36 | if (osorted.anchor < tsorted.edge && osorted.edge >= tsorted.anchor) 37 | return true; 38 | if (tsorted.anchor < osorted.edge && tsorted.edge >= osorted.anchor) 39 | return true; 40 | return false; 41 | } 42 | 43 | int opCmp(ref Selection other) pure nothrow 44 | { 45 | Selection tsorted = this.sorted(); 46 | Selection osorted = other.sorted(); 47 | if (tsorted.anchor.cursor.line != osorted.anchor.cursor.line) 48 | return tsorted.anchor.cursor.line - osorted.anchor.cursor.line; 49 | if (tsorted.anchor.cursor.column != osorted.anchor.cursor.column) 50 | return tsorted.anchor.cursor.column - osorted.anchor.cursor.column; 51 | 52 | if (tsorted.edge.cursor.line != osorted.edge.cursor.line) 53 | return tsorted.edge.cursor.line - osorted.edge.cursor.line; 54 | if (tsorted.edge.cursor.column != osorted.edge.cursor.column) 55 | return tsorted.edge.cursor.column - osorted.edge.cursor.column; 56 | return 0; 57 | } 58 | 59 | Selection opOpAssign(string op)(int displacement) if (op == "+") 60 | { 61 | anchor += displacement; 62 | edge += displacement; 63 | return this; 64 | } 65 | 66 | // Returns a sleection with the anchor at the left and the edge at the right 67 | Selection sorted() pure nothrow 68 | { 69 | if (anchor <= edge) 70 | return this; 71 | else 72 | return Selection(edge, anchor); 73 | } 74 | 75 | bool isValid() pure const nothrow 76 | { 77 | return anchor.isValid() && edge.isValid(); 78 | } 79 | 80 | void translateByEdit(BufferIterator before, BufferIterator after) 81 | { 82 | Selection tsorted = this.sorted(); 83 | assert(before <= tsorted.anchor); 84 | 85 | import std.stdio; 86 | if (before.cursor.line != after.cursor.line) 87 | { 88 | anchor.cursor.line += after.cursor.line - before.cursor.line; 89 | edge.cursor.line += after.cursor.line - before.cursor.line; 90 | 91 | if (anchor.cursor.line == after.cursor.line) 92 | anchor.cursor.column = after.cursor.column; 93 | if (edge.cursor.line == after.cursor.line) 94 | edge.cursor.column = after.cursor.column; 95 | } 96 | 97 | if (before.cursor.line == anchor.cursor.line) 98 | { 99 | int dispBefore = anchor.cursor.column - before.cursor.column; 100 | anchor.cursor.column = after.cursor.column + dispBefore; 101 | } 102 | 103 | if (before.cursor.line == edge.cursor.line) 104 | { 105 | int dispBefore = edge.cursor.column - before.cursor.column; 106 | edge.cursor.column = after.cursor.column + dispBefore; 107 | } 108 | } 109 | } 110 | 111 | class SelectionSet 112 | { 113 | Selection[] selections; // sorted by date 114 | 115 | this(Buffer buffer) 116 | { 117 | // always have a cursor 118 | selections ~= Selection(BufferIterator(buffer, Cursor(0, 0))); 119 | } 120 | 121 | this(Selection[] savedSelections) 122 | { 123 | selections = savedSelections; 124 | } 125 | 126 | void removeDuplicate() 127 | { 128 | import std.algorithm; 129 | import std.array; 130 | selections = selections.uniq.array; 131 | } 132 | 133 | void normalize() 134 | { 135 | // sort selections 136 | import std.algorithm; 137 | sort!("a < b", SwapStrategy.stable)(selections); 138 | 139 | int i = 0; 140 | while (i < cast(int)(selections.length) - 1) 141 | { 142 | if (selections[i].overlaps(selections[i+1])) 143 | { 144 | // merge overlapping selections 145 | Selection A = selections[i].sorted(); 146 | Selection B = selections[i+1].sorted(); 147 | BufferIterator anchor = A.anchor < B.anchor ? A.anchor : B.anchor; 148 | BufferIterator edge = A.edge > B.edge ? A.edge : B.edge; 149 | selections = selections[0..i] ~ Selection(anchor, edge) ~ selections[i+2..$]; 150 | } 151 | else if (selections[i] == selections[i+1]) 152 | { 153 | // drop one of two identical selections 154 | selections = selections[0..i] ~ selections[i+1..$]; 155 | } 156 | else 157 | ++i; 158 | } 159 | } 160 | 161 | void keepOnlyFirst() 162 | { 163 | selections = selections[0..1]; 164 | selections[0].anchor = selections[0].edge; 165 | } 166 | 167 | void keepOnlyEdge() 168 | { 169 | foreach(ref sel; selections) 170 | sel.anchor = sel.edge; 171 | } 172 | 173 | void clampSelections() 174 | { 175 | foreach(ref sel; selections) 176 | { 177 | sel.anchor.cursor = sel.anchor.buffer.clampCursor(sel.anchor.cursor); 178 | sel.edge.cursor = sel.edge.buffer.clampCursor(sel.edge.cursor); 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /source/dido/gui/element.d: -------------------------------------------------------------------------------- 1 | module dido.gui.element; 2 | 3 | public import dido.gui.context; 4 | 5 | class UIElement 6 | { 7 | public: 8 | 9 | this(UIContext context) 10 | { 11 | _context = context; 12 | } 13 | 14 | void close() 15 | { 16 | foreach(child; children) 17 | child.close(); 18 | } 19 | 20 | final void render() 21 | { 22 | if (!_visible) 23 | return; 24 | 25 | setViewportToElement(); 26 | preRender(_context.renderer); 27 | foreach(ref child; children) 28 | child.render(); 29 | setViewportToElement(); 30 | postRender(_context.renderer); 31 | } 32 | 33 | /// Meant to be overriden for custom behaviour. 34 | void reflow(box2i availableSpace) 35 | { 36 | // default: span the entire available area, and do the same for children 37 | _position = availableSpace; 38 | 39 | foreach(ref child; children) 40 | child.reflow(availableSpace); 41 | } 42 | 43 | final box2i position() 44 | { 45 | return _position; 46 | } 47 | 48 | final ref UIElement[] children() 49 | { 50 | return _children; 51 | } 52 | 53 | final Font font() 54 | { 55 | return _context.font; 56 | } 57 | 58 | final int charWidth() pure const nothrow 59 | { 60 | return _context.font.charWidth(); 61 | } 62 | 63 | final int charHeight() pure const nothrow 64 | { 65 | return _context.font.charHeight(); 66 | } 67 | 68 | final UIElement child(int n) 69 | { 70 | return _children[n]; 71 | } 72 | 73 | final void addChild(UIElement element) 74 | { 75 | _children ~= element; 76 | } 77 | 78 | // This function is meant to be overriden. 79 | // It should return true if the click is handled. 80 | bool onMousePostClick(int x, int y, int button, bool isDoubleClick) 81 | { 82 | return false; 83 | } 84 | 85 | // This function is meant to be overriden. 86 | // Happens _before_ checking for children collisions. 87 | bool onMousePreClick(int x, int y, int button, bool isDoubleClick) 88 | { 89 | return false; 90 | } 91 | 92 | // This function is meant to be overriden. 93 | // It should return true if the wheel is handled. 94 | bool onMouseWheel(int x, int y, int wheelDeltaX, int wheelDeltaY) 95 | { 96 | return false; 97 | } 98 | 99 | // Called when mouse move over this Element. 100 | void onMouseMove(int x, int y, int dx, int dy) 101 | { 102 | } 103 | 104 | // Called when clicked with left/middle/right button 105 | void onBeginDrag() 106 | { 107 | } 108 | 109 | // Called when mouse drag this Element. 110 | void onMouseDrag(int x, int y, int dx, int dy) 111 | { 112 | } 113 | 114 | // Called once drag is finished 115 | void onStopDrag() 116 | { 117 | } 118 | 119 | // Called when mouse enter this Element. 120 | void onMouseEnter() 121 | { 122 | } 123 | 124 | // Called when mouse enter this Element. 125 | void onMouseExit() 126 | { 127 | } 128 | 129 | // to be called at top-level when the mouse clicked 130 | bool mouseClick(int x, int y, int button, bool isDoubleClick) 131 | { 132 | if (_position.contains(vec2i(x, y))) 133 | { 134 | if(onMousePreClick(x - _position.min.x, y - _position.min.y, button, isDoubleClick)) 135 | { 136 | _context.beginDragging(this); 137 | return true; 138 | } 139 | } 140 | 141 | foreach(child; _children) 142 | { 143 | if (child.mouseClick(x, y, button, isDoubleClick)) 144 | return true; 145 | } 146 | 147 | if (_position.contains(vec2i(x, y))) 148 | { 149 | if(onMousePostClick(x - _position.min.x, y - _position.min.y, button, isDoubleClick)) 150 | { 151 | _context.beginDragging(this); 152 | return true; 153 | } 154 | } 155 | 156 | return false; 157 | } 158 | 159 | // to be called when the mouse is released 160 | final void mouseRelease(int x, int y, int button) 161 | { 162 | _context.stopDragging(); 163 | } 164 | 165 | // to be called when the mouse clicked 166 | final bool mouseWheel(int x, int y, int wheelDeltaX, int wheelDeltaY) 167 | { 168 | foreach(child; _children) 169 | { 170 | if (child.mouseWheel(x, y, wheelDeltaX, wheelDeltaY)) 171 | return true; 172 | } 173 | 174 | if (_position.contains(vec2i(x, y))) 175 | { 176 | if (onMouseWheel(x - _position.min.x, y - _position.min.y, wheelDeltaX, wheelDeltaY)) 177 | return true; 178 | } 179 | 180 | return false; 181 | } 182 | 183 | // to be called when the mouse moved 184 | final void mouseMove(int x, int y, int dx, int dy) 185 | { 186 | if (isDragged) 187 | onMouseDrag(x, y, dx, dy); 188 | 189 | foreach(child; _children) 190 | { 191 | child.mouseMove(x, y, dx, dy); 192 | } 193 | 194 | if (_position.contains(vec2i(x, y))) 195 | { 196 | if (!_mouseOver) 197 | onMouseEnter(); 198 | onMouseMove(x - _position.min.x, y - _position.min.y, dx, dy); 199 | _mouseOver = true; 200 | } 201 | else 202 | { 203 | if (_mouseOver) 204 | onMouseExit(); 205 | _mouseOver = false; 206 | } 207 | } 208 | 209 | final UIContext context() 210 | { 211 | return _context; 212 | } 213 | 214 | final bool isVisible() 215 | { 216 | return _visible; 217 | } 218 | 219 | final void setVisible(bool visible) 220 | { 221 | _visible = visible; 222 | } 223 | 224 | protected: 225 | 226 | /// Render this element before children. 227 | /// Meant to be overriden. 228 | void preRender(SDL2Renderer renderer) 229 | { 230 | // defaults to nothing 231 | } 232 | 233 | /// Render this element after children elements. 234 | /// Meant to be overriden. 235 | void postRender(SDL2Renderer renderer) 236 | { 237 | // defaults to nothing 238 | } 239 | 240 | box2i _position; 241 | 242 | UIElement[] _children; 243 | 244 | bool _visible = true; 245 | 246 | 247 | final bool isMouseOver() pure const nothrow 248 | { 249 | return _mouseOver; 250 | } 251 | 252 | final bool isDragged() pure const nothrow 253 | { 254 | return _context.dragged is this; 255 | } 256 | 257 | 258 | private: 259 | UIContext _context; 260 | 261 | bool _mouseOver = false; 262 | 263 | final void setViewportToElement() 264 | { 265 | _context.renderer.setViewport(_position.min.x, _position.min.y, _position.width, _position.height); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /source/dido/app.d: -------------------------------------------------------------------------------- 1 | module dido.app; 2 | 3 | import std.path; 4 | import std.file; 5 | import std.conv; 6 | import std.string; 7 | 8 | import gfm.sdl2; 9 | import gfm.math; 10 | 11 | import dido.buffer.buffer; 12 | import dido.window; 13 | import dido.buffer.selection; 14 | import dido.panel; 15 | import dido.gui; 16 | import dido.engine; 17 | import dido.config; 18 | import dido.colors; 19 | 20 | 21 | final class App 22 | { 23 | public: 24 | this(DidoConfig config, string[] dubPaths) 25 | { 26 | _sdl2 = new SDL2(null); 27 | _window = new Window(_sdl2); 28 | 29 | // font path is relative to application directory 30 | string appDir = dirName(thisExePath()); 31 | string fontFile = buildPath(appDir, config.fontFace); 32 | _font = new Font(_sdl2, _window.renderer(), fontFile, config.fontSize); 33 | 34 | _sdl2.startTextInput(); 35 | 36 | _uiContext = new UIContext(_sdl2, _window.renderer(), _font); 37 | 38 | { 39 | import dido.panel.images; 40 | addAllImage(_uiContext); 41 | } 42 | 43 | _textArea = new TextArea(_uiContext, 16, true, true, rgba(20, 19, 18, 255)); 44 | 45 | _cmdlinePanel = new CommandLinePanel(_uiContext); 46 | _outputPanel = new OutputPanel(_uiContext); 47 | 48 | _engine = new DidoEngine(_sdl2, _window, _textArea, _cmdlinePanel, _outputPanel, dubPaths); 49 | 50 | _solutionPanel = new SolutionPanel(_uiContext, _engine.projects()); 51 | _mainPanel = new MainPanel(_uiContext); 52 | _menuPanel = new MenuPanel(_uiContext, _engine); 53 | _westPanel = new WestPanel(_uiContext, _solutionPanel, _outputPanel); 54 | auto eastPanel = new EastPanel(_uiContext, _engine, _textArea); 55 | 56 | // Ugly setter to get the inter-dependent objects live 57 | _cmdlinePanel.updateEngine(_engine); 58 | 59 | _mainPanel.addChild(eastPanel); 60 | _mainPanel.addChild(_westPanel); 61 | _mainPanel.addChild(_cmdlinePanel); 62 | _mainPanel.addChild(_menuPanel); 63 | _mainPanel.addChild(new UIImage(_uiContext, "corner")); 64 | 65 | _needReflow = true; 66 | _needRedraw = true; 67 | } 68 | 69 | ~this() 70 | { 71 | _mainPanel.destroy(); 72 | _uiContext.destroy(); 73 | _sdl2.stopTextInput(); 74 | _font.destroy(); 75 | _window.destroy(); 76 | _sdl2.destroy(); 77 | } 78 | 79 | void mainLoop() 80 | { 81 | uint caretBlinkTime = 530 ; // default value on Win7 82 | uint caretCycleTime = caretBlinkTime * 2; // default value on Win7 83 | 84 | uint lastTime = SDL_GetTicks(); 85 | 86 | _timeSinceKeypress = 0; 87 | 88 | bool lastdrawCursors = false; 89 | 90 | bool firstFrame = true; 91 | 92 | while(!_sdl2.wasQuitRequested() && !_engine.finished()) 93 | { 94 | uint time = SDL_GetTicks(); 95 | uint deltaTime = time - lastTime; 96 | lastTime = time; 97 | _timeSinceKeypress += deltaTime; 98 | 99 | if(!firstFrame) 100 | waitForAnEventThenProcessThemAll(); 101 | 102 | firstFrame = false; 103 | 104 | SDL2Renderer renderer = _window.renderer(); 105 | 106 | int width = _window.getWidth(); 107 | int height = _window.getHeight(); 108 | int charWidth = _font.charWidth(); 109 | int charHeight = _font.charHeight(); 110 | 111 | bool drawCursors = (_timeSinceKeypress % caretCycleTime) < caretBlinkTime; 112 | 113 | if (lastdrawCursors != drawCursors) 114 | _needRedraw = true; 115 | 116 | lastdrawCursors = drawCursors; 117 | 118 | // reflow 119 | if (_needReflow) 120 | { 121 | _solutionPanel.updateState(_engine.projectSelect); 122 | _mainPanel.reflow(box2i(0, 0, width, height)); 123 | 124 | _needReflow = false; 125 | } 126 | 127 | // redraw 128 | if (_needRedraw) 129 | { 130 | _cmdlinePanel.updateMode(_engine.isCommandLineMode()); 131 | _cmdlinePanel.updateCursorState(drawCursors); 132 | _textArea.setState(_engine.currentEditBuffer(), !_engine.isCommandLineMode() && drawCursors); 133 | renderer.setViewportFull(); 134 | _mainPanel.render(); 135 | renderer.present(); 136 | } 137 | } 138 | } 139 | 140 | 141 | private: 142 | 143 | DidoEngine _engine; 144 | SDL2 _sdl2; 145 | Window _window; 146 | 147 | uint _timeSinceKeypress; 148 | 149 | MainPanel _mainPanel; 150 | MenuPanel _menuPanel; 151 | CommandLinePanel _cmdlinePanel; 152 | SolutionPanel _solutionPanel; 153 | OutputPanel _outputPanel; 154 | WestPanel _westPanel; 155 | TextArea _textArea; 156 | Font _font; 157 | UIContext _uiContext; 158 | 159 | bool _needReflow; 160 | bool _needRedraw; 161 | 162 | void dealWithEvent(ref SDL_Event event) 163 | { 164 | switch (event.type) 165 | { 166 | case SDL_KEYDOWN: 167 | { 168 | _timeSinceKeypress = 0; 169 | auto key = event.key.keysym; 170 | bool alt = (key.mod & KMOD_ALT) != 0; 171 | bool shift = (key.mod & KMOD_SHIFT) != 0; 172 | bool ctrl = (key.mod & KMOD_CTRL) != 0; 173 | string sshift = shift ? "#t" : "#f"; 174 | 175 | if (key.sym == SDLK_RETURN && alt) 176 | _engine.executeScheme("(toggle-fullscreen)"); 177 | else if (key.sym == SDLK_UP && ctrl && alt) 178 | _engine.executeScheme("(extend-selection-vertical -1)"); 179 | else if (key.sym == SDLK_DOWN && ctrl && alt) 180 | _engine.executeScheme("(extend-selection-vertical 1)"); 181 | else if (key.sym == SDLK_LEFT && ctrl) 182 | _engine.executeScheme(format("(move-word-left %s)", sshift)); 183 | else if (key.sym == SDLK_RIGHT && ctrl) 184 | _engine.executeScheme(format("(move-word-right %s)", sshift)); 185 | else if (key.sym == SDLK_ESCAPE) 186 | _engine.executeScheme("(escape)"); 187 | else if (key.sym == SDLK_RETURN) 188 | _engine.enter(); 189 | 190 | // copy/cut/paste 191 | else if (key.sym == SDLK_c && ctrl) 192 | _engine.executeScheme("(copy)"); 193 | else if (key.sym == SDLK_x && ctrl) 194 | _engine.executeScheme("(cut)"); 195 | else if (key.sym == SDLK_v && ctrl) 196 | _engine.executeScheme("(paste)"); 197 | else if (key.sym == SDLK_COPY) 198 | _engine.executeScheme("(copy)"); 199 | else if (key.sym == SDLK_CUT) 200 | _engine.executeScheme("(cut)"); 201 | else if (key.sym == SDLK_PASTE) 202 | _engine.executeScheme("(paste)"); 203 | else if (key.sym == SDLK_DELETE && shift) 204 | _engine.executeScheme("(cut)"); 205 | else if (key.sym == SDLK_INSERT && ctrl) 206 | _engine.executeScheme("(copy)"); 207 | else if (key.sym == SDLK_INSERT && shift) 208 | _engine.executeScheme("(paste)"); 209 | else if (key.sym == SDLK_LEFT) 210 | _engine.executeScheme(format("(move-horizontal -1 %s)", sshift)); 211 | else if (key.sym == SDLK_RIGHT) 212 | _engine.executeScheme(format("(move-horizontal 1 %s)", sshift)); 213 | else if (key.sym == SDLK_UP) 214 | _engine.executeScheme(format("(move-vertical -1 %s)", sshift)); 215 | else if (key.sym == SDLK_DOWN) 216 | _engine.executeScheme(format("(move-vertical 1 %s)", sshift)); 217 | else if (key.sym == SDLK_BACKSPACE) 218 | _engine.executeScheme("(delete-selection #t)"); 219 | else if (key.sym == SDLK_DELETE) 220 | _engine.executeScheme("(delete-selection #f)"); 221 | else if (key.sym == SDLK_HOME && ctrl) 222 | _engine.executeScheme(format("(move-buffer-start %s)", sshift)); 223 | else if (key.sym == SDLK_END && ctrl) 224 | _engine.executeScheme(format("(move-buffer-end %s)", sshift)); 225 | else if (key.sym == SDLK_a && ctrl) 226 | _engine.executeScheme("(select-all)"); 227 | else if (key.sym == SDLK_END) 228 | _engine.executeScheme(format("(move-line-end %s)", sshift)); 229 | else if (key.sym == SDLK_HOME) 230 | _engine.executeScheme(format("(move-line-start %s)", sshift)); 231 | else if (key.sym == SDLK_PAGEUP && ctrl) 232 | _engine.executeScheme("(previous-buffer)"); 233 | else if (key.sym == SDLK_PAGEDOWN && ctrl) 234 | _engine.executeScheme("(next-buffer)"); 235 | else if (key.sym == SDLK_PAGEUP) 236 | _engine.executeScheme(format("(move-vertical (- (visible-lines)) %s)", sshift)); 237 | else if (key.sym == SDLK_PAGEDOWN) 238 | _engine.executeScheme(format("(move-vertical (visible-lines) %s)", sshift)); 239 | else if (key.sym == SDLK_z && ctrl) 240 | _engine.executeScheme("(undo)"); 241 | else if (key.sym == SDLK_y && ctrl) 242 | _engine.executeScheme("(redo)"); 243 | else if (key.sym == SDLK_s && ctrl) 244 | _engine.executeScheme("(save)"); 245 | else if ( (key.sym == SDLK_F5 && shift) || (key.sym == SDLK_PAUSE && ctrl) ) 246 | _engine.executeScheme("(stop-build)"); 247 | else if (key.sym == SDLK_F4) 248 | _engine.executeScheme("(build current-compiler current-arch current-build)"); 249 | else if (key.sym == SDLK_F5) 250 | _engine.executeScheme("(run current-compiler current-arch current-build)"); 251 | else if (key.sym == SDLK_TAB) 252 | _engine.executeScheme("(indent)"); 253 | else 254 | { 255 | } 256 | 257 | _needReflow = true; 258 | _needRedraw = true; 259 | break; 260 | } 261 | 262 | case SDL_TEXTINPUT: 263 | { 264 | _timeSinceKeypress = 0; 265 | const(char)[] s = fromStringz(event.text.text.ptr); 266 | 267 | if (s == ":") 268 | _engine.toggleCommandLineMode(); 269 | else 270 | { 271 | dstring ds = to!dstring(s); 272 | foreach(ch; ds) 273 | _engine.executeScheme(format("(insert-char %s)", to!int(ch))); 274 | } 275 | } 276 | _needRedraw = true; 277 | break; 278 | 279 | 280 | case SDL_MOUSEBUTTONDOWN: 281 | { 282 | const (SDL_MouseButtonEvent*) mbEvent = &event.button; 283 | 284 | // undo 285 | if (mbEvent.button == SDL_BUTTON_X1) 286 | _engine.executeScheme("(undo)"); 287 | else if (mbEvent.button == SDL_BUTTON_X2) 288 | _engine.executeScheme("(redo)"); 289 | else 290 | _mainPanel.mouseClick(_sdl2.mouse.x, _sdl2.mouse.y, mbEvent.button, mbEvent.clicks > 1); 291 | _needRedraw = true; 292 | break; 293 | } 294 | 295 | case SDL_MOUSEBUTTONUP: 296 | { 297 | const (SDL_MouseButtonEvent*) mbEvent = &event.button; 298 | _mainPanel.mouseRelease(_sdl2.mouse.x, _sdl2.mouse.y, mbEvent.button); 299 | _needRedraw = true; 300 | break; 301 | } 302 | 303 | case SDL_MOUSEWHEEL: 304 | { 305 | _mainPanel.mouseWheel(_sdl2.mouse.x, _sdl2.mouse.y, _sdl2.mouse.wheelDeltaX(), _sdl2.mouse.wheelDeltaY()); 306 | _needRedraw = true; 307 | break; 308 | } 309 | 310 | case SDL_MOUSEMOTION: 311 | _mainPanel.mouseMove(_sdl2.mouse.x, _sdl2.mouse.y, _sdl2.mouse.lastDeltaX(), _sdl2.mouse.lastDeltaY()); 312 | _needRedraw = true; 313 | break; 314 | 315 | case SDL_WINDOWEVENT: 316 | /*{ 317 | const (SDL_WindowEvent*) windowEvent = &event.window; 318 | 319 | if (windowEvent.event == ) {*/ 320 | 321 | 322 | _needReflow = true; 323 | _needRedraw = true; 324 | break; 325 | 326 | 327 | default: 328 | break; 329 | } 330 | } 331 | 332 | void waitForAnEventThenProcessThemAll() 333 | { 334 | SDL_Event event; 335 | if (_sdl2.waitEventTimeout(&event, 50)) 336 | { 337 | dealWithEvent(event); 338 | while (_sdl2.pollEvent(&event)) 339 | { 340 | dealWithEvent(event); 341 | } 342 | } 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /source/dido/panel/textarea.d: -------------------------------------------------------------------------------- 1 | module dido.panel.textarea; 2 | 3 | import std.conv : to; 4 | import std.algorithm : min, max; 5 | 6 | import gfm.sdl2; 7 | import gfm.math.vector; 8 | 9 | import dido.buffer.selection; 10 | import dido.gui; 11 | import dido.buffer.buffer; 12 | import dido.buffer.bufferiterator; 13 | 14 | class TextArea : UIElement 15 | { 16 | public: 17 | 18 | this(UIContext context, int marginEditor, bool haveLineNumbers, bool hasScrollbars, vec4ub backgroundColor) 19 | { 20 | super(context); 21 | 22 | _marginEditor = marginEditor; 23 | 24 | if (hasScrollbars) 25 | { 26 | addChild(new VertScrollBar(context, 9, 4, true)); 27 | verticalScrollbar = cast(ScrollBar) child(0); 28 | 29 | addChild(new HorzScrollBar(context, 9, 4, false)); 30 | horizontalScrollbar = cast(ScrollBar) child(1); 31 | } 32 | 33 | if (haveLineNumbers) 34 | { 35 | lineNumberArea = new LineNumberArea(context); 36 | addChild(lineNumberArea); 37 | } 38 | 39 | _editCursor = new SDL2Cursor(context.sdl2, SDL_SYSTEM_CURSOR_IBEAM); 40 | _previousCursor = SDL2Cursor.getCurrent(context.sdl2); 41 | _backgroundColor = backgroundColor; 42 | } 43 | 44 | override void close() 45 | { 46 | _editCursor.close(); 47 | _previousCursor.close(); 48 | foreach(ref child; children) 49 | child.close(); 50 | } 51 | 52 | override void reflow(box2i availableSpace) 53 | { 54 | if (lineNumberArea !is null) 55 | { 56 | lineNumberArea.reflow(availableSpace); 57 | availableSpace.min.x = lineNumberArea.position.max.x; 58 | } 59 | 60 | if (verticalScrollbar !is null) 61 | { 62 | box2i availableForVert = availableSpace; 63 | verticalScrollbar.reflow(availableForVert); 64 | availableSpace.max.x = verticalScrollbar.position.min.x; 65 | } 66 | 67 | if (horizontalScrollbar !is null) 68 | { 69 | horizontalScrollbar.reflow(availableSpace); 70 | } 71 | _position = availableSpace; 72 | } 73 | 74 | // Returns number of simultaneously visible lines 75 | int numVisibleLines() pure const nothrow 76 | { 77 | int result = (_position.height - 16) / charHeight; 78 | if (result < 1) 79 | result = 1; 80 | return result; 81 | } 82 | 83 | override void preRender(SDL2Renderer renderer) 84 | { 85 | if (_backgroundColor.a != 0) 86 | { 87 | renderer.setColor(_backgroundColor.r, _backgroundColor.g, _backgroundColor.b, _backgroundColor.a); 88 | renderer.fillRect(0, 0, _position.width, _position.height); 89 | } 90 | 91 | int editPosX = -_cameraX + _marginEditor; 92 | int editPosY = -_cameraY + _marginEditor; 93 | 94 | int firstVisibleLine = getFirstVisibleLine(); 95 | int firstNonVisibleLine = getFirstNonVisibleLine(); 96 | 97 | int firstVisibleColumn = getFirstVisibleColumn(); 98 | int firstNonVisibleColumn = getFirstNonVisibleColumn(); 99 | int longestLineLength = _buffer.getLongestLineLength(); 100 | 101 | box2i visibleBox = box2i(0, 0, _position.width, _position.height); 102 | 103 | for (int i = firstVisibleLine; i < firstNonVisibleLine; ++i) 104 | { 105 | dstring line = _buffer.line(i); 106 | 107 | int posXInChars = 0; 108 | int posY = editPosY + i * charHeight; 109 | 110 | int maxCol = min(line.length, firstNonVisibleColumn); 111 | 112 | // Allows to draw cursor on the very last file position 113 | if ( i + 1 == _buffer.numLines() ) 114 | maxCol++; 115 | 116 | for(int j = firstVisibleColumn; j < maxCol; ++j) 117 | { 118 | Buffer.Hit hit = _buffer.intersectsSelection(Cursor(i, j)); 119 | bool charIsSelected = hit.charInSelection; 120 | 121 | dchar ch = j < line.length ? line[j] : ' '; // last line read a little to far 122 | int widthOfChar = 1; 123 | bool drawDot = false; 124 | switch (ch) 125 | { 126 | case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 127 | font.setColor(255, 200, 200); 128 | break; 129 | 130 | case '+', '-', '=', '>', '<', '^', ',', '$', '|', '&', '`', '/', '@', '.', '"', '[', ']', '?', ':', '\'', '\\': 131 | font.setColor(255, 255, 106); 132 | break; 133 | 134 | case '(', ')', ';': 135 | font.setColor(255, 255, 150); 136 | break; 137 | 138 | case '{': 139 | font.setColor(108, 108, 128); 140 | break; 141 | 142 | case '}': 143 | font.setColor(108, 108, 138); 144 | break; 145 | 146 | case '\n': 147 | ch = ' '; 148 | break; 149 | 150 | case '\t': 151 | ch = 0x2192; 152 | font.setColor(80, 60, 70); 153 | int tabLength = 1;//4; not working yet 154 | widthOfChar = tabLength - posXInChars % tabLength; 155 | break; 156 | 157 | case ' ': 158 | if (charIsSelected) 159 | { 160 | drawDot = true; 161 | } 162 | break; 163 | 164 | default: 165 | font.setColor(250, 250, 250); 166 | break; 167 | } 168 | 169 | int posX = editPosX + posXInChars * charWidth; 170 | box2i charBox = box2i(posX, posY, posX + charWidth, posY + charHeight); 171 | 172 | if (visibleBox.intersects(charBox)) 173 | { 174 | if (charIsSelected) 175 | { 176 | // draw selection background 177 | renderer.setColor(43, 54, 66, 255); 178 | renderer.fillRect(charBox.min.x, charBox.min.y, charBox.width, charBox.height); 179 | } 180 | 181 | if (drawDot) 182 | { 183 | renderer.setColor(131, 137, 152, 255); 184 | renderer.fillRect(charBox.min.x + charBox.width/2, charBox.min.y + charBox.height/2, 1, 1); 185 | } 186 | else if (ch != ' ') 187 | font.renderChar(ch, posX, posY); 188 | 189 | if (hit.cursorThere && _drawCursors) 190 | { 191 | // draw cursor 192 | renderer.setColor(255, 255, 255, 255); 193 | renderer.fillRect(charBox.min.x, charBox.min.y, 1, charHeight - 1); 194 | } 195 | } 196 | posXInChars += widthOfChar; 197 | } 198 | } 199 | 200 | if (lineNumberArea !is null) 201 | { 202 | lineNumberArea.setState(_buffer, _marginEditor, firstVisibleLine, firstNonVisibleLine, _cameraY); 203 | } 204 | 205 | if (verticalScrollbar !is null) 206 | { 207 | int actualRange = maxCameraY() + _position.height; 208 | float start = _cameraY / cast(float)(actualRange); 209 | float stop = (_cameraY + _position.height) / cast(float)(actualRange); 210 | verticalScrollbar.setProgress(start, stop); 211 | 212 | // annoying grey rect in scrollbar crossing 213 | int scrollSize = verticalScrollbar.position.width; 214 | renderer.setViewport(_position.min.x, _position.min.y, _position.width + scrollSize, _position.height + scrollSize); 215 | renderer.setColor(0x30, 0x2C, 0x2C, 255); 216 | renderer.fillRect(_position.width, _position.height, scrollSize, scrollSize); 217 | } 218 | 219 | if (horizontalScrollbar !is null) 220 | { 221 | int actualRange = maxCameraX() + _position.width; 222 | float start = _cameraX / cast(float)(actualRange); 223 | float stop = (_cameraX + _position.width) / cast(float)(actualRange); 224 | horizontalScrollbar.setProgress(start, stop); 225 | } 226 | } 227 | 228 | void setState(Buffer buffer, bool drawCursors) 229 | { 230 | _buffer = buffer; 231 | _drawCursors = drawCursors; 232 | } 233 | 234 | void clearCamera() 235 | { 236 | _cameraX = 0; 237 | _cameraY = 0; 238 | } 239 | 240 | void moveCamera(int dx, int dy) 241 | { 242 | if (_buffer is null) 243 | return; 244 | 245 | _cameraX += dx; 246 | _cameraY += dy; 247 | normalizeCamera(); 248 | } 249 | 250 | override bool onMouseWheel(int x, int y, int wheelDeltaX, int wheelDeltaY) 251 | { 252 | moveCamera(-wheelDeltaX * 3 * charWidth, -wheelDeltaY * 3 * charHeight); 253 | return true; 254 | } 255 | 256 | int maxCameraX() 257 | { 258 | int maxLineLength = _buffer.getLongestLineLength(); 259 | return max(0, maxLineLength * charWidth - _position.width); 260 | } 261 | 262 | int maxCameraY() 263 | { 264 | return _buffer.numLines() * charHeight; 265 | } 266 | 267 | void normalizeCamera() 268 | { 269 | if (_cameraX < 0) 270 | _cameraX = 0; 271 | 272 | if (_cameraX > maxCameraX()) 273 | _cameraX = maxCameraX(); 274 | 275 | if (_cameraY < 0) 276 | _cameraY = 0; 277 | 278 | if (_cameraY > maxCameraY()) 279 | _cameraY = maxCameraY(); 280 | } 281 | 282 | box2i cameraBox() pure const nothrow 283 | { 284 | return box2i(_cameraX, _cameraY, _cameraX + _position.width, _cameraY + _position.height); 285 | } 286 | 287 | box2i edgeBox(Selection sel) 288 | { 289 | return box2i(_cameraX, _cameraY, _cameraX + _position.width, _cameraY + _position.height); 290 | } 291 | 292 | void ensureOneVisibleSelection() 293 | { 294 | double minDistance = double.infinity; 295 | Selection bestSel; 296 | SelectionSet selset = _buffer.selectionSet(); 297 | foreach(Selection sel; selset.selections) 298 | { 299 | double distance = selectionDistance(sel); 300 | if (distance < minDistance) 301 | { 302 | bestSel = sel; 303 | minDistance = distance; 304 | } 305 | } 306 | ensureSelectionVisible(bestSel); 307 | } 308 | 309 | override void onMouseEnter() 310 | { 311 | _editCursor.setCurrent(); 312 | } 313 | 314 | override void onMouseExit() 315 | { 316 | _previousCursor.setCurrent(); 317 | } 318 | 319 | override bool onMousePostClick(int x, int y, int button, bool isDoubleClick) 320 | { 321 | if (_buffer is null) 322 | return false; 323 | 324 | // implement click on buffer and CTRL + click 325 | if (button == SDL_BUTTON_LEFT || button == SDL_BUTTON_RIGHT) 326 | { 327 | bool ctrl = context.sdl2.keyboard.isPressed(SDLK_LCTRL) || context.sdl2.keyboard.isPressed(SDLK_RCTRL); 328 | 329 | int line = (y - _marginEditor + _cameraY) / charHeight; 330 | int column = (x - _marginEditor + _cameraX + (charWidth / 2)) / charWidth; 331 | 332 | _buffer.addNewSelection(line, column, ctrl); 333 | return true; 334 | } 335 | 336 | return false; 337 | } 338 | 339 | override void onMouseDrag(int x, int y, int dx, int dy) 340 | { 341 | if (_buffer is null) 342 | return; 343 | 344 | // implement drag selection 345 | { 346 | // TODO, need selection timestamps, or some way to have a selection outside of Buffer (better) 347 | 348 | } 349 | } 350 | 351 | class VertScrollBar : ScrollBar 352 | { 353 | public: 354 | this(UIContext context, int widthOfFocusBar, int padding, bool vertical) 355 | { 356 | super(context, widthOfFocusBar, padding, vertical); 357 | } 358 | 359 | override void onScrollChangeMouse(float newProgressStart) 360 | { 361 | int actualRange = maxCameraY() + _position.height; 362 | _cameraY = cast(int)(0.5f + newProgressStart * actualRange); 363 | normalizeCamera(); 364 | } 365 | } 366 | 367 | class HorzScrollBar : ScrollBar 368 | { 369 | public: 370 | this(UIContext context, int widthOfFocusBar, int padding, bool vertical) 371 | { 372 | super(context, widthOfFocusBar, padding, vertical); 373 | } 374 | 375 | override void onScrollChangeMouse(float newProgressStart) 376 | { 377 | int actualRange = maxCameraX() + _position.width; 378 | _cameraX = cast(int)(0.5f + newProgressStart * actualRange); 379 | normalizeCamera(); 380 | } 381 | } 382 | 383 | private: 384 | int _cameraX = 0; 385 | int _cameraY = 0; 386 | Buffer _buffer; 387 | bool _drawCursors; 388 | 389 | vec4ub _backgroundColor; 390 | 391 | SDL2Cursor _editCursor; 392 | SDL2Cursor _previousCursor; 393 | 394 | int _marginEditor; 395 | 396 | LineNumberArea lineNumberArea; 397 | ScrollBar verticalScrollbar; 398 | ScrollBar horizontalScrollbar; 399 | 400 | int getFirstVisibleLine() pure const nothrow 401 | { 402 | return max(0, _cameraY / charHeight - 1); 403 | } 404 | 405 | int getFirstVisibleColumn() pure const nothrow 406 | { 407 | return max(0, _cameraX / charWidth - 1); 408 | } 409 | 410 | int getFirstNonVisibleLine() pure const nothrow 411 | { 412 | return min(_buffer.numLines(), 1 + (_cameraY + _position.height + charHeight - 1) / charHeight); 413 | } 414 | 415 | int getFirstNonVisibleColumn() pure const nothrow 416 | { 417 | return min(_buffer.getLongestLineLength(), 1 + (_cameraX + _position.width + charWidth - 1) / charWidth); 418 | } 419 | 420 | box2i getEdgeBox(Selection selection) 421 | { 422 | vec2i edgePos = vec2i(selection.edge.cursor.column * charWidth + _marginEditor, 423 | selection.edge.cursor.line * charHeight + _marginEditor); 424 | return box2i(edgePos.x, edgePos.y, edgePos.x + charWidth, edgePos.y + charHeight); 425 | } 426 | 427 | // 0 if visible 428 | // more if not visible 429 | double selectionDistance(Selection selection) 430 | { 431 | return cameraBox().distance(getEdgeBox(selection)); 432 | } 433 | 434 | void ensureSelectionVisible(Selection selection) 435 | { 436 | int scrollMargin = _marginEditor; 437 | box2i edgeBox = getEdgeBox(selection); 438 | box2i camBox = cameraBox(); 439 | if (edgeBox.min.x < camBox.min.x + scrollMargin) 440 | _cameraX += (edgeBox.min.x - camBox.min.x - scrollMargin); 441 | 442 | if (edgeBox.max.x > camBox.max.x - scrollMargin) 443 | _cameraX += (edgeBox.max.x - camBox.max.x + scrollMargin); 444 | 445 | if (edgeBox.min.y < camBox.min.y + scrollMargin) 446 | _cameraY += (edgeBox.min.y - camBox.min.y - scrollMargin); 447 | if (edgeBox.max.y > camBox.max.y - scrollMargin) 448 | _cameraY += (edgeBox.max.y - camBox.max.y + scrollMargin); 449 | normalizeCamera(); 450 | } 451 | 452 | void renderSelectionForeground(SDL2Renderer renderer, int offsetX, int offsetY, Selection selection, bool drawCursors) 453 | { 454 | Selection sorted = selection.sorted(); 455 | 456 | // don't draw invisible selections 457 | if (sorted.edge.cursor.line < getFirstVisibleLine()) 458 | return; 459 | 460 | if (sorted.anchor.cursor.line >= getFirstNonVisibleLine()) 461 | return; 462 | 463 | 464 | if (drawCursors) 465 | { 466 | int startX = offsetX + selection.edge.cursor.column * charWidth; 467 | int startY = offsetY + selection.edge.cursor.line * charHeight; 468 | 469 | renderer.setColor(255, 255, 255, 255); 470 | renderer.fillRect(startX, startY, 1, charHeight - 1); 471 | } 472 | } 473 | } 474 | 475 | 476 | class LineNumberArea : UIElement 477 | { 478 | public: 479 | 480 | this(UIContext context) 481 | { 482 | super(context); 483 | } 484 | 485 | override void reflow(box2i availableSpace) 486 | { 487 | _position = availableSpace; 488 | _position.max.x = _position.min.x + 6 * charWidth; 489 | } 490 | 491 | override void preRender(SDL2Renderer renderer) 492 | { 493 | renderer.setColor(28, 28, 28, 255); 494 | renderer.fillRect(0, 0, _position.width, _position.height); 495 | 496 | for (int i = _firstVisibleLine; i < _firstNonVisibleLine; ++i) 497 | { 498 | dstring lineNumber = to!dstring(i + 1) ~ " "; 499 | while (lineNumber.length < 6) 500 | { 501 | lineNumber = " "d ~ lineNumber; 502 | } 503 | 504 | font.setColor(49, 97, 107, 160); 505 | font.renderString(lineNumber, 0, 0 -_cameraY + _marginEditor + i * charHeight); 506 | } 507 | } 508 | 509 | void setState(Buffer buffer, int marginEditor, int firstVisibleLine, int firstNonVisibleLine, int cameraY) 510 | { 511 | _buffer = buffer; 512 | _cameraY = cameraY; 513 | _firstVisibleLine = firstVisibleLine; 514 | _firstNonVisibleLine = firstNonVisibleLine; 515 | _marginEditor = marginEditor; 516 | } 517 | 518 | private: 519 | Buffer _buffer; 520 | int _cameraY; 521 | int _marginEditor; 522 | int _firstVisibleLine; 523 | int _firstNonVisibleLine; 524 | } 525 | 526 | 527 | -------------------------------------------------------------------------------- /source/dido/engine.d: -------------------------------------------------------------------------------- 1 | module dido.engine; 2 | 3 | import std.process; 4 | import std.conv; 5 | import std.string; 6 | import std.concurrency; 7 | 8 | import gfm.sdl2; 9 | 10 | import dido.panel; 11 | import dido.buffer.buffer; 12 | import dido.window; 13 | import dido.builder; 14 | import dido.project; 15 | 16 | import schemed; 17 | 18 | // Model 19 | class DidoEngine 20 | { 21 | private: 22 | TextArea _textArea; 23 | CommandLinePanel _cmdlinePanel; 24 | OutputPanel _outputPanel; 25 | bool _commandLineMode; 26 | Buffer _bufferEdit; // current buffer in edit area 27 | Project[] _projects; 28 | 29 | int _projectSelect; 30 | 31 | Window _window; 32 | SDL2 _sdl2; 33 | bool _finished; 34 | Builder _builder; 35 | 36 | // scheme-d environment 37 | Environment _env; 38 | 39 | public: 40 | 41 | this(SDL2 sdl2, Window window, TextArea textArea, CommandLinePanel cmdlinePanel, OutputPanel outputPanel, string[] dubPaths) 42 | { 43 | foreach (ref dubPath; dubPaths) 44 | { 45 | Project project = new Project(dubPath); 46 | _projects ~= project; 47 | } 48 | 49 | setCurrentProject(0); 50 | currentProject.setCurrentBuffer(0); 51 | showCurrentBuffer(); 52 | 53 | _textArea = textArea; 54 | _outputPanel = outputPanel; 55 | _cmdlinePanel = cmdlinePanel; 56 | _window = window; 57 | _commandLineMode = false; 58 | _finished = false; 59 | _sdl2 = sdl2; 60 | 61 | _env = defaultEnvironment(); 62 | addBuiltins(_env); 63 | 64 | _builder = new Builder(this); 65 | } 66 | 67 | void showCurrentBuffer() 68 | { 69 | setEditableBuffer(currentProject.currentBuffer); 70 | } 71 | 72 | Project[] projects() 73 | { 74 | return _projects; 75 | } 76 | 77 | bool isCommandLineMode() 78 | { 79 | return _commandLineMode; 80 | } 81 | 82 | // change the current editable 83 | void setEditableBuffer(Buffer buffer) 84 | { 85 | _bufferEdit = buffer; 86 | _bufferEdit.ensureLoaded(); 87 | } 88 | 89 | // change current edited buffer 90 | void setCurrentProject(int projectSelect) 91 | { 92 | _projectSelect = projectSelect; 93 | } 94 | 95 | int projectSelect() 96 | { 97 | return _projectSelect; 98 | } 99 | 100 | Project currentProject() 101 | { 102 | return _projects[_projectSelect]; 103 | } 104 | 105 | Buffer currentEditBuffer() 106 | { 107 | return _bufferEdit; 108 | } 109 | 110 | bool finished() 111 | { 112 | return _finished; 113 | } 114 | 115 | Buffer currentBuffer() 116 | { 117 | if (_commandLineMode) 118 | return _cmdlinePanel.buffer(); 119 | else 120 | return _bufferEdit; 121 | } 122 | 123 | TextArea currentTextArea() 124 | { 125 | if (_commandLineMode) 126 | return _cmdlinePanel.textArea(); 127 | else 128 | return _textArea; 129 | } 130 | 131 | void enterCommandLineMode() 132 | { 133 | if (!_commandLineMode) 134 | { 135 | _commandLineMode = true; 136 | currentBuffer().clearContent(); 137 | } 138 | } 139 | 140 | void leaveCommandLineMode() 141 | { 142 | if (_commandLineMode) 143 | { 144 | _commandLineMode = false; 145 | currentTextArea().ensureOneVisibleSelection(); 146 | } 147 | } 148 | 149 | void toggleCommandLineMode() 150 | { 151 | if (!_commandLineMode) 152 | enterCommandLineMode(); 153 | else 154 | leaveCommandLineMode(); 155 | } 156 | 157 | void logMessage(LineType type, dstring msg) 158 | { 159 | _outputPanel.log(LineOutput(type, msg)); 160 | } 161 | 162 | void greenMessage(dstring msg) 163 | { 164 | _outputPanel.log(LineOutput(LineType.SUCCESS, msg)); 165 | } 166 | 167 | void redMessage(dstring msg) 168 | { 169 | _outputPanel.log(LineOutput(LineType.ERROR, msg)); 170 | } 171 | 172 | void executeScheme(string code, bool echo = false) 173 | { 174 | if (echo) 175 | _outputPanel.log(LineOutput(LineType.COMMAND, ":"d ~ to!dstring(code))); 176 | 177 | Atom result; 178 | try 179 | { 180 | result = Atom(execute(code, _env)); // result is discarded 181 | } 182 | catch(SchemeParseException e) 183 | { 184 | // try to execute again, but with parens appended 185 | try 186 | { 187 | result = Atom(execute("(" ~ code ~ ")", _env)); 188 | } 189 | catch(SchemeParseException e2) 190 | { 191 | // another error, print the _first_ message 192 | redMessage(to!dstring(e.msg)); 193 | return; 194 | } 195 | catch(SchemeEvalException e2) 196 | { 197 | redMessage(to!dstring(e2.msg)); 198 | return; 199 | } 200 | } 201 | catch(SchemeEvalException e) 202 | { 203 | redMessage(to!dstring(e.msg)); 204 | return; 205 | } 206 | 207 | // output result 208 | if (echo) 209 | _outputPanel.log(LineOutput(LineType.RESULT, to!dstring("=> " ~ result.toString))); 210 | } 211 | 212 | void executeCommandLine(dstring cmdline) 213 | { 214 | if (cmdline == ""d) 215 | _bufferEdit.insertChar(':'); 216 | else 217 | { 218 | executeScheme(to!string(cmdline), true); 219 | } 220 | } 221 | 222 | void enter() 223 | { 224 | // TODO implement this logic in Scheme itself 225 | if (_commandLineMode) 226 | { 227 | executeCommandLine(_cmdlinePanel.getCommandLine()); 228 | _commandLineMode = false; 229 | } 230 | else 231 | { 232 | currentBuffer.insertChar('\n'); 233 | currentTextArea.ensureOneVisibleSelection(); 234 | } 235 | } 236 | 237 | bool checkArgs(string func, Atom[] args, int min, int max) 238 | { 239 | if (args.length < min) 240 | { 241 | redMessage(to!dstring(format("%s expects %s to %s arguments", to!string(func), min, max))); 242 | return false; 243 | } 244 | else if (args.length > max) 245 | { 246 | redMessage(to!dstring(format("%s expects %s to %s arguments", to!string(func), min, max))); 247 | return false; 248 | } 249 | else 250 | return true; 251 | } 252 | 253 | void addBuiltins(Environment env) 254 | { 255 | env.addBuiltin("display", (Atom[] args) 256 | { 257 | foreach(arg; args) 258 | _outputPanel.log(LineOutput(LineType.RESULT, to!dstring(arg.toString))); 259 | return makeNil(); 260 | }); 261 | 262 | env.addBuiltin("exit", (Atom[] args) 263 | { 264 | if (!checkArgs("q|exit", args, 0, 0)) 265 | return makeNil(); 266 | _finished = true; 267 | greenMessage("Bye"d); 268 | return makeNil(); 269 | }); 270 | 271 | env.addBuiltin("undo", (Atom[] args) 272 | { 273 | if (!checkArgs("u|undo", args, 0, 0)) 274 | return makeNil(); 275 | currentBuffer().undo(); 276 | currentTextArea().ensureOneVisibleSelection(); 277 | return makeNil(); 278 | }); 279 | 280 | env.addBuiltin("redo", (Atom[] args) 281 | { 282 | if (!checkArgs("r|redo", args, 0, 0)) 283 | return makeNil(); 284 | currentBuffer().redo(); 285 | currentTextArea().ensureOneVisibleSelection(); 286 | return makeNil(); 287 | }); 288 | 289 | env.addBuiltin("new", (Atom[] args) 290 | { 291 | if (!checkArgs("n|new", args, 0, 0)) 292 | return makeNil(); 293 | currentProject().buffers() ~= new Buffer; 294 | currentProject().setCurrentBuffer(currentProject().numBuffers() - 1); 295 | showCurrentBuffer(); 296 | greenMessage("Created new file"d); 297 | return makeNil(); 298 | }); 299 | 300 | env.addBuiltin("clean", (Atom[] args) 301 | { 302 | if (!checkArgs("clean", args, 0, 0)) 303 | return makeNil(); 304 | _bufferEdit.cleanup(); 305 | greenMessage("Buffer cleaned up"d); 306 | return makeNil(); 307 | }); 308 | 309 | env.addBuiltin("save", (Atom[] args) 310 | { 311 | if (!checkArgs("s|save", args, 0, 0)) 312 | return makeNil(); 313 | 314 | if (_bufferEdit.isBoundToFileName()) 315 | { 316 | string filepath = _bufferEdit.filePath(); 317 | _bufferEdit.saveToFile(filepath); 318 | greenMessage(to!dstring(format("Saved to %s", filepath))); 319 | } 320 | else 321 | { 322 | redMessage("This buffer is unbounded, try :save "); 323 | } 324 | return makeNil(); 325 | }); 326 | 327 | env.addBuiltin("load", (Atom[] args) 328 | { 329 | if (!checkArgs("l|load", args, 0, 0)) 330 | return makeNil(); 331 | 332 | if (_bufferEdit.isBoundToFileName()) 333 | { 334 | string filepath = _bufferEdit.filePath(); 335 | _bufferEdit.loadFromFile(filepath); 336 | greenMessage(to!dstring(format("Loaded %s", filepath))); 337 | } 338 | else 339 | redMessage("This buffer is unbounded, try :load "); 340 | return makeNil(); 341 | }); 342 | 343 | env.addBuiltin("stop-build", (Atom[] args) 344 | { 345 | if (!checkArgs("stop-build", args, 0, 0)) 346 | return makeNil(); 347 | _builder.stopBuild(); 348 | return makeNil(); 349 | }); 350 | 351 | env.addBuiltin("build", (Atom[] args) 352 | { 353 | if (!checkArgs("build", args, 3, 3)) 354 | return makeNil(); 355 | 356 | string compiler = args[0].toString(); 357 | string arch = args[1].toString(); 358 | string build = args[2].toString(); 359 | _builder.startBuild(compiler, arch, build); 360 | return makeNil(); 361 | }); 362 | 363 | env.addBuiltin("run", (Atom[] args) 364 | { 365 | if (!checkArgs("run", args, 3, 3)) 366 | return makeNil(); 367 | 368 | string compiler = args[0].toString(); 369 | string arch = args[1].toString(); 370 | string build = args[2].toString(); 371 | _builder.startRun(compiler, arch, build); 372 | return makeNil(); 373 | }); 374 | 375 | env.addBuiltin("test", (Atom[] args) 376 | { 377 | if (!checkArgs("test", args, 3, 3)) 378 | return makeNil(); 379 | 380 | string compiler = args[0].toString(); 381 | string arch = args[1].toString(); 382 | string build = args[2].toString(); 383 | _builder.startTest(compiler, arch, build); 384 | return makeNil(); 385 | }); 386 | 387 | env.addBuiltin("copy", (Atom[] args) 388 | { 389 | if (!checkArgs("copy", args, 0, 0)) 390 | return makeNil(); 391 | _sdl2.setClipboard(to!string(currentBuffer.copy())); 392 | currentTextArea.ensureOneVisibleSelection(); 393 | return makeNil(); 394 | }); 395 | 396 | env.addBuiltin("cut", (Atom[] args) 397 | { 398 | if (!checkArgs("cut", args, 0, 0)) 399 | return makeNil(); 400 | _sdl2.setClipboard(to!string(currentBuffer.cut())); 401 | currentTextArea.ensureOneVisibleSelection(); 402 | return makeNil(); 403 | }); 404 | 405 | env.addBuiltin("paste", (Atom[] args) 406 | { 407 | if (!checkArgs("paste", args, 0, 0)) 408 | return makeNil(); 409 | dstring clipboard = to!dstring(_sdl2.getClipboard()); 410 | currentBuffer.paste(clipboard); 411 | currentTextArea.ensureOneVisibleSelection(); 412 | return makeNil(); 413 | }); 414 | 415 | 416 | // aliases 417 | env.values["n"] = env.values["new"]; 418 | env.values["s"] = env.values["save"]; 419 | env.values["l"] = env.values["load"]; 420 | env.values["u"] = env.values["undo"]; 421 | env.values["r"] = env.values["redo"]; 422 | env.values["q"] = env.values["exit"]; 423 | 424 | env.addBuiltin("visible-lines", (Atom[] args) 425 | { 426 | if (!checkArgs("visible-lines", args, 0, 0)) 427 | return makeNil(); 428 | double lines = _textArea.numVisibleLines; 429 | return Atom(lines); 430 | }); 431 | 432 | env.addBuiltin("move-vertical", (Atom[] args) 433 | { 434 | if (!checkArgs("move-vertical", args, 2, 2)) 435 | return makeNil(); 436 | int displacement = to!int(args[0].toDouble); 437 | bool shift = args[1].toBool; 438 | currentBuffer.moveSelectionVertical(displacement, shift); 439 | currentTextArea.ensureOneVisibleSelection(); 440 | return makeNil(); 441 | }); 442 | 443 | env.addBuiltin("move-horizontal", (Atom[] args) 444 | { 445 | if (!checkArgs("move-horizontal", args, 2, 2)) 446 | return makeNil(); 447 | int displacement = to!int(args[0].toDouble); 448 | bool shift = args[1].toBool; 449 | currentBuffer.moveSelectionHorizontal(displacement, shift); 450 | currentTextArea.ensureOneVisibleSelection(); 451 | return makeNil(); 452 | }); 453 | 454 | env.addBuiltin("move-buffer-start", (Atom[] args) 455 | { 456 | if (!checkArgs("move-buffer-start", args, 1, 1)) 457 | return makeNil(); 458 | bool shift = args[0].toBool; 459 | currentBuffer.moveSelectionToBufferStart(shift); 460 | currentTextArea.ensureOneVisibleSelection(); 461 | return makeNil(); 462 | }); 463 | 464 | env.addBuiltin("move-buffer-end", (Atom[] args) 465 | { 466 | if (!checkArgs("move-buffer-end", args, 1, 1)) 467 | return makeNil(); 468 | bool shift = args[0].toBool; 469 | currentBuffer.moveSelectionToBufferEnd(shift); 470 | currentTextArea.ensureOneVisibleSelection(); 471 | return makeNil(); 472 | }); 473 | 474 | env.addBuiltin("move-word-left", (Atom[] args) 475 | { 476 | if (!checkArgs("move-word-left", args, 1, 1)) 477 | return makeNil(); 478 | bool shift = args[0].toBool; 479 | currentBuffer.moveSelectionWord(-1, shift); 480 | currentTextArea.ensureOneVisibleSelection(); 481 | return makeNil(); 482 | }); 483 | 484 | env.addBuiltin("move-word-right", (Atom[] args) 485 | { 486 | if (!checkArgs("move-word-right", args, 1, 1)) 487 | return makeNil(); 488 | bool shift = args[0].toBool; 489 | currentBuffer.moveSelectionWord(1, shift); 490 | currentTextArea.ensureOneVisibleSelection(); 491 | return makeNil(); 492 | }); 493 | 494 | env.addBuiltin("move-line-start", (Atom[] args) 495 | { 496 | if (!checkArgs("move-line-start", args, 1, 1)) 497 | return makeNil(); 498 | bool shift = args[0].toBool; 499 | currentBuffer.moveToLineBegin(shift); 500 | currentTextArea.ensureOneVisibleSelection(); 501 | return makeNil(); 502 | }); 503 | 504 | env.addBuiltin("move-line-end", (Atom[] args) 505 | { 506 | if (!checkArgs("move-line-end", args, 1, 1)) 507 | return makeNil(); 508 | bool shift = args[0].toBool; 509 | currentBuffer.moveToLineEnd(shift); 510 | currentTextArea.ensureOneVisibleSelection(); 511 | return makeNil(); 512 | }); 513 | 514 | env.addBuiltin("extend-selection-vertical", (Atom[] args) 515 | { 516 | if (!checkArgs("extend-selection-vertical", args, 1, 1)) 517 | return makeNil(); 518 | int displacement = to!int(args[0].toDouble); 519 | currentBuffer.extendSelectionVertical(displacement); 520 | currentTextArea.ensureOneVisibleSelection(); 521 | return makeNil(); 522 | }); 523 | 524 | env.addBuiltin("select-all", (Atom[] args) 525 | { 526 | if (!checkArgs("select-all", args, 0, 0)) 527 | return makeNil(); 528 | currentBuffer.selectAll(); 529 | currentTextArea.ensureOneVisibleSelection(); 530 | return makeNil(); 531 | }); 532 | 533 | env.addBuiltin("next-project", (Atom[] args) 534 | { 535 | if (!checkArgs("next-project", args, 0, 0)) 536 | return makeNil(); 537 | setCurrentProject((_projectSelect + 1) % cast(int) _projects.length ); 538 | showCurrentBuffer(); 539 | currentTextArea().clearCamera(); 540 | currentTextArea().ensureOneVisibleSelection(); 541 | return makeNil(); 542 | }); 543 | 544 | env.addBuiltin("previous-project", (Atom[] args) 545 | { 546 | if (!checkArgs("previous-project", args, 0, 0)) 547 | return makeNil(); 548 | setCurrentProject((_projectSelect + cast(int) _projects.length - 1) % cast(int) _projects.length ); 549 | showCurrentBuffer(); 550 | currentTextArea().clearCamera(); 551 | currentTextArea().ensureOneVisibleSelection(); 552 | return makeNil(); 553 | }); 554 | 555 | env.addBuiltin("next-buffer", (Atom[] args) 556 | { 557 | if (!checkArgs("next-buffer", args, 0, 0)) 558 | return makeNil(); 559 | currentProject.nextBuffer(); 560 | showCurrentBuffer(); 561 | currentTextArea().clearCamera(); 562 | currentTextArea().ensureOneVisibleSelection(); 563 | return makeNil(); 564 | }); 565 | 566 | env.addBuiltin("previous-buffer", (Atom[] args) 567 | { 568 | if (!checkArgs("previous-buffer", args, 0, 0)) 569 | return makeNil(); 570 | currentProject.previousBuffer(); 571 | showCurrentBuffer(); 572 | currentTextArea().clearCamera(); 573 | currentTextArea().ensureOneVisibleSelection(); 574 | return makeNil(); 575 | }); 576 | 577 | env.addBuiltin("toggle-fullscreen", (Atom[] args) 578 | { 579 | if (!checkArgs("toggle-fullscreen", args, 0, 0)) 580 | return makeNil(); 581 | _window.toggleFullscreen(); 582 | currentTextArea.ensureOneVisibleSelection(); 583 | return makeNil(); 584 | }); 585 | 586 | env.addBuiltin("escape", (Atom[] args) 587 | { 588 | if (!checkArgs("escape", args, 0, 0)) 589 | return makeNil(); 590 | if (_commandLineMode) 591 | _commandLineMode = false; 592 | else 593 | { 594 | currentBuffer.selectionSet().keepOnlyFirst(); 595 | currentTextArea.ensureOneVisibleSelection(); 596 | } 597 | return makeNil(); 598 | }); 599 | 600 | env.addBuiltin("indent", (Atom[] args) 601 | { 602 | if (!checkArgs("indent", args, 0, 0)) 603 | return makeNil(); 604 | currentBuffer.insertTab(); 605 | currentTextArea.ensureOneVisibleSelection(); 606 | return makeNil(); 607 | }); 608 | 609 | env.addBuiltin("delete-selection", (Atom[] args) 610 | { 611 | if (!checkArgs("delete", args, 1, 1)) 612 | return makeNil(); 613 | bool isBackspace = args[0].toBool; 614 | currentBuffer.deleteSelection(isBackspace); 615 | currentTextArea.ensureOneVisibleSelection(); 616 | return makeNil(); 617 | }); 618 | 619 | env.addBuiltin("insert-char", (Atom[] args) 620 | { 621 | if (!checkArgs("insert-char", args, 1, 1)) 622 | return makeNil(); 623 | dchar ch = to!dchar(to!int(args[0].toDouble)); 624 | currentBuffer.insertChar(ch); 625 | currentTextArea.ensureOneVisibleSelection(); 626 | return makeNil(); 627 | }); 628 | } 629 | } 630 | -------------------------------------------------------------------------------- /source/dido/buffer/buffer.d: -------------------------------------------------------------------------------- 1 | module dido.buffer.buffer; 2 | 3 | import std.file; 4 | import std.string; 5 | import std.array; 6 | import std.conv; 7 | import std.algorithm; 8 | 9 | import dido.buffer.selection; 10 | import dido.buffer.buffercommand; 11 | import dido.buffer.bufferiterator; 12 | 13 | 14 | // text buffers 15 | 16 | final class Buffer 17 | { 18 | private: 19 | string _filepath; 20 | SelectionSet _selectionSet; 21 | dstring[] lines; 22 | int _historyIndex; // where we are in history 23 | BufferCommand[] _history; 24 | bool _hasBeenLoaded; 25 | int _longestLine; 26 | 27 | public: 28 | 29 | // create new 30 | this() 31 | { 32 | lines = ["\n"d]; 33 | _selectionSet = new SelectionSet(this); 34 | _filepath = null; 35 | _hasBeenLoaded = true; 36 | updateLongestLine(); 37 | } 38 | 39 | this(string filepath) 40 | { 41 | lines = ["\n"d]; 42 | _selectionSet = new SelectionSet(this); 43 | _filepath = filepath; 44 | _hasBeenLoaded = false; 45 | updateLongestLine(); 46 | } 47 | 48 | void clearContent() 49 | { 50 | lines = ["\n"d]; 51 | _selectionSet = new SelectionSet(this); 52 | _filepath = null; 53 | _hasBeenLoaded = true; 54 | updateLongestLine(); 55 | } 56 | 57 | 58 | void ensureLoaded() 59 | { 60 | if (!_hasBeenLoaded) 61 | { 62 | assert(_filepath !is null); 63 | loadFromFile(_filepath); 64 | _hasBeenLoaded = true; 65 | } 66 | } 67 | 68 | // load file in buffer, non-conforming utf-8 is lost 69 | void loadFromFile(string path) 70 | { 71 | lines = readTextFile(path, _longestLine); 72 | } 73 | 74 | ubyte[] toSource() 75 | { 76 | ubyte[] result; 77 | for (size_t i = 0; i < lines.length; ++i) 78 | { 79 | string line = to!string(lines[i]); 80 | 81 | // don't output virtual EOL of last line 82 | assert(line.length > 0); 83 | if (i + 1 == lines.length) 84 | line = line[0..$-1]; 85 | 86 | version(Windows) 87 | { 88 | if (line.length > 0 && line[$-1] == '\n') 89 | line = line[0..$-1] ~ "\r\n"; 90 | result ~= line; 91 | } 92 | } 93 | return result; 94 | } 95 | 96 | dstring getContent() 97 | { 98 | return getSelectionContent(Selection(begin(), end())); 99 | } 100 | 101 | // save file using OS end-of-lines 102 | void saveToFile(string path) 103 | { 104 | std.file.write(path, toSource()); 105 | } 106 | 107 | int numLines() pure const nothrow 108 | { 109 | return cast(int)(lines.length); 110 | } 111 | 112 | int lineLength(int lineIndex) pure const nothrow 113 | { 114 | return cast (int) lines[lineIndex].length; 115 | } 116 | 117 | int getLongestLineLength() pure const nothrow 118 | { 119 | return lineLength(_longestLine); 120 | } 121 | 122 | // maximum column allowed for cursor on this line 123 | int maxColumn(int lineIndex) pure const nothrow 124 | { 125 | return cast(int) lines[lineIndex].length - 1; 126 | } 127 | 128 | inout(dstring) line(int lineIndex) pure inout nothrow 129 | { 130 | return lines[lineIndex]; 131 | } 132 | 133 | dstring cut() 134 | { 135 | dstring result = copy(); 136 | deleteSelection(false); 137 | return result; 138 | } 139 | 140 | dstring copy() 141 | { 142 | dstring result = ""d; 143 | int sellen = cast(int)_selectionSet.selections.length; 144 | for (int i = 0; i < sellen; ++i) 145 | { 146 | if (i > 0) 147 | result ~= '\n'; 148 | result ~= getSelectionContent(_selectionSet.selections[i]); 149 | } 150 | return result; 151 | } 152 | 153 | void paste(dstring clipboardContent) 154 | { 155 | // removes \r from clipboard string (system might add it for interop reasons) 156 | dstring clearedClipBoard; 157 | int l = 0; 158 | for (int i = 0; i < cast(int)(clipboardContent.length); ++i) 159 | if (clipboardContent[i] != '\r') 160 | clearedClipBoard ~= clipboardContent[i]; 161 | 162 | if (_selectionSet.selections.length > 1) 163 | { 164 | dstring[] byLineContent = splitLines(clearedClipBoard); 165 | bySelectionEdit( (int i) 166 | { 167 | if (i < byLineContent.length) 168 | return byLineContent[i]; 169 | else 170 | return ""d; 171 | } ); 172 | } 173 | else 174 | { 175 | bySelectionEdit( (int i) 176 | { 177 | return clearedClipBoard; 178 | } ); 179 | } 180 | } 181 | 182 | void undo() 183 | { 184 | while (_historyIndex > 0) 185 | { 186 | _historyIndex--; 187 | if (reverseCommand(_history[_historyIndex])) 188 | break; 189 | } 190 | } 191 | 192 | void redo() 193 | { 194 | int barrierFound = 0; 195 | while (_historyIndex < _history.length) 196 | { 197 | if (_history[_historyIndex].type == BufferCommandType.BARRIER) 198 | barrierFound++; 199 | 200 | if (barrierFound >= 2) 201 | return; 202 | 203 | applyCommand(_history[_historyIndex]); 204 | _historyIndex++; 205 | } 206 | } 207 | 208 | bool isBoundToFileName() 209 | { 210 | return _filepath !is null; 211 | } 212 | 213 | void moveSelectionHorizontal(int dx, bool shift) 214 | { 215 | foreach(ref sel; _selectionSet.selections) 216 | { 217 | sel.edge = sel.edge + dx; 218 | 219 | if (!shift) 220 | sel.anchor = sel.edge; 221 | } 222 | _selectionSet.normalize(); 223 | } 224 | 225 | void moveSelectionVertical(int dy, bool shift) 226 | { 227 | foreach(ref sel; _selectionSet.selections) 228 | { 229 | sel.edge.cursor = clampCursor(Cursor(sel.edge.cursor.line + dy, sel.edge.cursor.column)); 230 | 231 | if (!shift) 232 | sel.anchor = sel.edge; 233 | } 234 | _selectionSet.normalize(); 235 | } 236 | 237 | // move by words 238 | void moveSelectionWord(int dx, bool shift) 239 | { 240 | assert(dx == -1 || dx == 1); 241 | foreach(ref sel; _selectionSet.selections) 242 | { 243 | BufferIterator edge = sel.edge; 244 | 245 | if (dx == -1 && edge.canBeDecremented) 246 | --edge; 247 | 248 | static bool isSpace(dchar ch) 249 | { 250 | static immutable dstring spaces = " \n\t\r"d; 251 | for (size_t i = 0; i < spaces.length; ++i) 252 | if (ch == spaces[i]) 253 | return true; 254 | return false; 255 | } 256 | 257 | static bool isOperator(dchar ch) 258 | { 259 | static immutable dstring operators = "+-=><^,$|&`/@.\"[](){}?:\'\\"d; 260 | for (size_t i = 0; i < operators.length; ++i) 261 | if (ch == operators[i]) 262 | return true; 263 | return false; 264 | } 265 | 266 | static bool isAlnum(dchar ch) 267 | { 268 | return !(isSpace(ch) || isOperator(ch)); 269 | } 270 | 271 | bool wentThroughLineBreak = false; 272 | 273 | while(true) 274 | { 275 | dchar ch = edge.read(); 276 | if (ch == '\n') 277 | wentThroughLineBreak = true; 278 | if (! (isSpace(ch) && edge.canGoInDirection(dx)) ) 279 | break; 280 | edge += dx; 281 | } 282 | 283 | if (!wentThroughLineBreak) 284 | { 285 | bool isOp = isOperator(edge.read); 286 | if (isOp) 287 | { 288 | while( isOperator(edge.read) && edge.canGoInDirection(dx) ) 289 | edge += dx; 290 | } 291 | else 292 | { 293 | while( isAlnum(edge.read) && edge.canGoInDirection(dx) ) 294 | edge += dx; 295 | } 296 | } 297 | 298 | if (dx == -1 && edge.canBeIncremented) 299 | ++edge; 300 | 301 | sel.edge = edge; 302 | if (!shift) 303 | sel.anchor = sel.edge; 304 | _selectionSet.normalize(); 305 | } 306 | } 307 | 308 | 309 | void moveSelectionToBufferStart(bool shift) 310 | { 311 | foreach(ref sel; _selectionSet.selections) 312 | { 313 | sel.edge = begin(); 314 | if (!shift) 315 | sel.anchor = sel.edge; 316 | } 317 | _selectionSet.normalize(); 318 | } 319 | 320 | void moveSelectionToBufferEnd(bool shift) 321 | { 322 | foreach(ref sel; _selectionSet.selections) 323 | { 324 | sel.edge = end(); 325 | if (!shift) 326 | sel.anchor = sel.edge; 327 | } 328 | _selectionSet.normalize(); 329 | } 330 | 331 | void selectAll() 332 | { 333 | foreach(ref sel; _selectionSet.selections) 334 | { 335 | sel.anchor = begin(); 336 | sel.edge = end(); 337 | } 338 | _selectionSet.normalize(); 339 | } 340 | 341 | Cursor clampCursor(Cursor cursor) 342 | { 343 | Cursor result; 344 | result.line = min(max(cursor.line, 0), numLines() - 1); 345 | result.column = min(max(cursor.column, 0), maxColumn(result.line)); 346 | return result; 347 | } 348 | 349 | // Add a new area-less selection 350 | void extendSelectionVertical(int dy) 351 | { 352 | Selection sel; 353 | if (dy > 0) 354 | sel = _selectionSet.selections[$-1]; 355 | else 356 | sel = _selectionSet.selections[0]; 357 | 358 | sel.edge.cursor = clampCursor(Cursor(sel.edge.cursor.line + dy, sel.edge.cursor.column)); 359 | 360 | sel.anchor = sel.edge; 361 | _selectionSet.selections ~= sel; 362 | _selectionSet.normalize(); 363 | } 364 | 365 | void addNewSelection(int line, int column, bool keepExistingSelections) 366 | { 367 | BufferIterator it = BufferIterator(this, clampCursor(Cursor(line, column))); 368 | Selection newSel = Selection(it, it); 369 | assert(newSel.isValid); 370 | 371 | if (keepExistingSelections) 372 | _selectionSet.selections ~= newSel; 373 | else 374 | _selectionSet.selections = [ newSel ]; 375 | _selectionSet.normalize(); 376 | } 377 | 378 | void moveToLineBegin(bool shift) 379 | { 380 | foreach(ref sel; _selectionSet.selections) 381 | { 382 | sel.edge.cursor.column = 0; 383 | 384 | if (!shift) 385 | sel.anchor = sel.edge; 386 | } 387 | _selectionSet.normalize(); 388 | } 389 | 390 | void moveToLineEnd(bool shift) 391 | { 392 | foreach(ref sel; _selectionSet.selections) 393 | { 394 | sel.edge.cursor.column = maxColumn(sel.edge.cursor.line); 395 | 396 | if (!shift) 397 | sel.anchor = sel.edge; 398 | } 399 | _selectionSet.normalize(); 400 | } 401 | 402 | inout(SelectionSet) selectionSet() inout 403 | { 404 | return _selectionSet; 405 | } 406 | 407 | void insertChar(dchar ch) 408 | { 409 | dstring content = ""d ~ ch; 410 | bySelectionEdit( (int i) 411 | { 412 | return content; 413 | } ); 414 | } 415 | 416 | void insertTab() 417 | { 418 | dstring content = " "d; 419 | bySelectionEdit( (int i) 420 | { 421 | return content; 422 | } ); 423 | } 424 | 425 | // selection with area => delete selection 426 | // else delete character at cursor or before cursor 427 | void deleteSelection(bool isBackspace) 428 | { 429 | enqueueBarrier(); 430 | enqueueSaveSelections(); 431 | 432 | for (int i = 0; i < _selectionSet.selections.length; ++i) 433 | { 434 | Selection selectionBeforeEdit = _selectionSet.selections[i]; 435 | Selection selectionAfterEdit; 436 | if (selectionBeforeEdit.hasSelectedArea()) 437 | { 438 | selectionAfterEdit = enqueueEdit(selectionBeforeEdit, ""d); 439 | } 440 | else 441 | { 442 | Selection oneCharSel = selectionBeforeEdit; 443 | if (isBackspace && oneCharSel.anchor.canBeDecremented) 444 | oneCharSel.anchor--; 445 | else if (oneCharSel.edge.canBeIncremented) 446 | oneCharSel.edge++; 447 | 448 | assert(oneCharSel.isValid()); 449 | if (oneCharSel.hasSelectedArea()) 450 | { 451 | selectionBeforeEdit = oneCharSel; 452 | selectionAfterEdit = enqueueEdit(oneCharSel, ""d); 453 | } 454 | else 455 | selectionAfterEdit = selectionBeforeEdit; 456 | } 457 | 458 | _selectionSet.selections[i] = selectionAfterEdit; 459 | 460 | // apply offset to all subsequent selections 461 | for (int j = i + 1; j < _selectionSet.selections.length; ++j) 462 | { 463 | _selectionSet.selections[j].translateByEdit(selectionBeforeEdit.sorted.edge, selectionAfterEdit.sorted.edge); 464 | } 465 | 466 | for (int j = 0; j < _selectionSet.selections.length; ++j) 467 | { 468 | assert(_selectionSet.selections[j].isValid()); 469 | } 470 | } 471 | _selectionSet.keepOnlyEdge(); 472 | _selectionSet.normalize(); 473 | enqueueSaveSelections(); 474 | } 475 | 476 | string filePath() 477 | { 478 | if (_filepath is null) 479 | return "Untitled"; 480 | else 481 | return _filepath; 482 | } 483 | 484 | void cleanup() 485 | { 486 | enqueueBarrier(); 487 | enqueueSaveSelections(); 488 | 489 | _selectionSet.keepOnlyFirst(); 490 | _selectionSet.selections[0].edge.cursor = Cursor(0, 0); 491 | _selectionSet.selections[0].anchor.cursor = Cursor(0, 0); 492 | 493 | for (int i = 0; i < lines.length; ++i) 494 | { 495 | bool whitespace = true; 496 | int start = i == lines.length - 1 ? cast(int)(lines[i].length) - 1 : cast(int)(lines[i].length) - 2; 497 | for (int j = start; j >= 0; --j) 498 | { 499 | dchar ch = lines[i][j]; 500 | if (whitespace && ch == ' ') 501 | { 502 | Selection sel = Selection(BufferIterator(this, Cursor(i, j)), BufferIterator(this, Cursor(i, j + 1))); 503 | enqueueEdit(sel, ""d); 504 | } 505 | else 506 | whitespace = false; 507 | 508 | if (ch == '\t') 509 | { 510 | Selection sel = Selection(BufferIterator(this, Cursor(i, j)), BufferIterator(this, Cursor(i, j + 1))); 511 | enqueueEdit(sel, " "d); 512 | } 513 | } 514 | } 515 | _selectionSet.normalize(); 516 | enqueueSaveSelections(); 517 | } 518 | 519 | struct Hit 520 | { 521 | bool charInSelection; 522 | bool cursorThere; 523 | Selection selInside; 524 | } 525 | 526 | // return a combination of Hit flags 527 | Hit intersectsSelection(Cursor cursor) 528 | { 529 | Hit hit; 530 | hit.charInSelection = false; 531 | hit.cursorThere = false; 532 | 533 | int min = 0; 534 | int max = cast(int) _selectionSet.selections.length - 1; 535 | while(min <= max) // assume sorted selections 536 | { 537 | int middle = (min + max + 1) / 2; 538 | Selection sel = _selectionSet.selections[middle]; 539 | Selection ssel = sel.sorted(); 540 | 541 | if (cursor == sel.edge.cursor) 542 | hit.cursorThere = true; 543 | 544 | if (cursor < ssel.anchor.cursor) 545 | { 546 | max = middle - 1; 547 | } 548 | else if (cursor >= ssel.edge.cursor) 549 | { 550 | min = middle + 1; 551 | } 552 | else if (cursor >= ssel.anchor.cursor && cursor < ssel.edge.cursor) 553 | { 554 | hit.charInSelection = true; 555 | hit.selInside = sel; 556 | break; 557 | } 558 | else 559 | assert(false); 560 | } 561 | return hit; 562 | } 563 | 564 | 565 | invariant() 566 | { 567 | if (_hasBeenLoaded) 568 | { 569 | // at least one line 570 | if (lines.length < 1) 571 | { 572 | assert(false); 573 | } 574 | 575 | // every line finish by a \n 576 | for (size_t i = 0; i < lines.length; ++i) 577 | { 578 | dstring l = lines[i]; 579 | if (l.length == 0) 580 | { 581 | assert(false); 582 | } 583 | if (l[$-1] != '\n') 584 | { 585 | assert(false); 586 | } 587 | } 588 | } 589 | } 590 | 591 | package: 592 | 593 | BufferIterator begin() 594 | { 595 | return BufferIterator(this, Cursor(0, 0)); 596 | } 597 | 598 | BufferIterator end() 599 | { 600 | return BufferIterator(this, Cursor(cast(int) lines.length - 1, maxColumn(cast(int) lines.length - 1))); 601 | } 602 | 603 | private: 604 | 605 | void updateLongestLine() pure nothrow 606 | { 607 | int maxLength = 0; 608 | _longestLine = 0; 609 | 610 | for (int i = 0; i < cast(int)lines.length; ++i) 611 | { 612 | if (lines[i].length > maxLength) 613 | { 614 | maxLength = cast(int) lines[i].length; 615 | _longestLine = i; 616 | } 617 | } 618 | } 619 | 620 | void pushCommand(BufferCommand command) 621 | { 622 | // strip previous history, add command 623 | _history = _history[0.._historyIndex] ~ command; 624 | _historyIndex = cast(int) _history.length; 625 | } 626 | 627 | // replace a Selection content by a new content 628 | // returns a cursor Selection just after the newly inserted part 629 | Selection replaceSelectionContent(Selection selection, dstring content) 630 | { 631 | Selection sel = selection.sorted(); 632 | erase(sel.anchor, sel.edge); 633 | BufferIterator after = insert(sel.anchor, content); 634 | return Selection(sel.anchor, after); 635 | } 636 | 637 | // Gets content of a selection 638 | dstring getSelectionContent(Selection selection) 639 | { 640 | Selection sel = selection.sorted(); 641 | dstring result; 642 | 643 | // TODO PERF this could be faster by appending larger chunks 644 | for (BufferIterator it = sel.anchor; it != sel.edge; ++it) 645 | result ~= it.read(); 646 | 647 | return result; 648 | } 649 | 650 | BufferIterator insert(BufferIterator pos, dstring content) 651 | { 652 | foreach(dchar ch ; content) 653 | pos = insert(pos, ch); 654 | return pos; 655 | } 656 | 657 | // insert a single char 658 | // return an iterator after the inserted char 659 | BufferIterator insert(BufferIterator pos, dchar content) 660 | { 661 | assert(pos.isValid()); 662 | 663 | if (content == '\n') 664 | { 665 | int col = pos.cursor.column; 666 | int line = pos.cursor.line; 667 | bool shouldUpdateLongestLine = (line == _longestLine); 668 | bool shouldAddOneToLongestLine = (line < _longestLine); 669 | dstring thisLine = lines[line]; 670 | lines.insertInPlace(line, thisLine[0..col].idup ~ '\n'); // copy sub-part addind a \n 671 | lines[line + 1] = lines[line + 1][col..$]; // next line becomes everything else 672 | 673 | // in case we broke the longest line, linear search of longest line 674 | if (shouldAddOneToLongestLine) 675 | _longestLine++; 676 | 677 | if (shouldUpdateLongestLine) 678 | updateLongestLine(); 679 | 680 | return BufferIterator(pos.buffer, Cursor(line + 1, 0)); 681 | } 682 | else 683 | { 684 | int line = pos.cursor.line; 685 | int column = pos.cursor.column; 686 | dstring oneCh = (&content)[0..1].idup; 687 | replaceInPlace(lines[line], column, column, oneCh); 688 | 689 | // check that longest line has changed 690 | if (lines[line].length > getLongestLineLength()) 691 | _longestLine = line; 692 | 693 | return BufferIterator(pos.buffer, Cursor(line, column + 1)); 694 | } 695 | } 696 | 697 | // delete a single char 698 | void erase(BufferIterator pos) 699 | { 700 | dchar chErased = pos.read(); 701 | if (chErased == '\n') 702 | { 703 | int line = pos.cursor.line; 704 | int column = pos.cursor.column; 705 | dstring newLine = lines[line][0..$-1] ~ lines[line+1]; 706 | replaceInPlace(lines, line, line + 2, [ newLine ]); 707 | updateLongestLine(); 708 | } 709 | else 710 | { 711 | int line = pos.cursor.line; 712 | int column = pos.cursor.column; 713 | replaceInPlace(lines[line], column, column + 1, ""d); 714 | 715 | // check that it's still the longest line 716 | if (_longestLine == line) 717 | updateLongestLine(); 718 | } 719 | } 720 | 721 | void erase(BufferIterator begin, BufferIterator end) 722 | { 723 | while (begin < end) 724 | { 725 | --end; 726 | erase(end); 727 | } 728 | } 729 | 730 | void applyCommand(BufferCommand command) 731 | { 732 | final switch(command.type) with (BufferCommandType) 733 | { 734 | case CHANGE_CHARS: 735 | replaceSelectionContent(command.changeChars.oldSel, command.changeChars.newContent); 736 | break; 737 | 738 | case SAVE_SELECTIONS: 739 | // also restore them, useful in redo sequences 740 | _selectionSet.selections = command.saveSelections.selections.dup; 741 | break; 742 | 743 | case BARRIER: 744 | // do nothing 745 | break; 746 | } 747 | } 748 | 749 | // returns true if it was a barrier 750 | bool reverseCommand(BufferCommand command) 751 | { 752 | final switch(command.type) with (BufferCommandType) 753 | { 754 | case CHANGE_CHARS: 755 | replaceSelectionContent(command.changeChars.newSel, command.changeChars.oldContent); 756 | return false; 757 | 758 | case SAVE_SELECTIONS: 759 | // restore selections 760 | _selectionSet.selections = command.saveSelections.selections.dup; 761 | return false; 762 | 763 | case BARRIER: 764 | return true; 765 | } 766 | } 767 | 768 | void enqueueBarrier() 769 | { 770 | pushCommand(barrierCommand()); 771 | } 772 | 773 | void enqueueSaveSelections() 774 | { 775 | pushCommand(saveSelectionsCommand(_selectionSet.selections)); 776 | } 777 | 778 | Selection enqueueEdit(Selection selection, dstring newContent) 779 | { 780 | dstring oldContent = getSelectionContent(selection); 781 | Selection newSel = replaceSelectionContent(selection, newContent); 782 | BufferCommand command = changeCharsCommand(selection, newSel, oldContent, newContent); 783 | pushCommand(command); 784 | return Selection(newSel.edge); 785 | } 786 | 787 | void bySelectionEdit(dstring delegate(int i) selectionContent) 788 | { 789 | enqueueBarrier(); 790 | enqueueSaveSelections(); 791 | 792 | for (int i = 0; i < _selectionSet.selections.length; ++i) 793 | { 794 | Selection selectionBeforeEdit = _selectionSet.selections[i]; 795 | Selection selectionAfterEdit = enqueueEdit(selectionBeforeEdit, selectionContent(i)).sorted(); 796 | 797 | // apply offset to all subsequent selections 798 | // TODO PERF this is quadratic behaviour 799 | for (int j = i + 1; j < _selectionSet.selections.length; ++j) 800 | { 801 | _selectionSet.selections[j].translateByEdit(selectionBeforeEdit.sorted.edge, selectionAfterEdit.sorted.edge); 802 | } 803 | _selectionSet.selections[i] = selectionAfterEdit; 804 | } 805 | _selectionSet.normalize(); 806 | enqueueSaveSelections(); 807 | } 808 | } 809 | 810 | private 811 | { 812 | 813 | 814 | // removes BOM, sanitize Unicode, and split on line endings 815 | dstring[] readTextFile(string path, out int longestLine) 816 | { 817 | string wholeFile = cast(string) std.file.read(path); 818 | 819 | // remove UTF-8 BOM 820 | if (wholeFile.length > 3 && wholeFile[0] == '\xEF' && wholeFile[1] == '\xBB' && wholeFile[2] == '\xBF') 821 | wholeFile = wholeFile[3..$]; 822 | 823 | // sanitize non-UTF-8 sequences 824 | import std.encoding : sanitize; 825 | wholeFile = sanitize(wholeFile); 826 | 827 | dstring wholeFileUTF32 = to!dstring(wholeFile); 828 | 829 | dstring[] lines; 830 | dstring currentLine; 831 | longestLine = 0; 832 | int maxLength = 0; 833 | int numLine = 0; 834 | 835 | for (size_t i = 0; i < wholeFileUTF32.length; ++i) 836 | { 837 | dchar ch = wholeFileUTF32[i]; 838 | 839 | if (ch == '\n') 840 | { 841 | currentLine ~= '\n'; 842 | if (currentLine.length > maxLength) 843 | { 844 | longestLine = numLine; 845 | maxLength = cast(int) currentLine.length; 846 | } 847 | lines ~= currentLine.dup; 848 | numLine++; 849 | currentLine.length = 0; 850 | } 851 | else if (ch == '\r') 852 | { 853 | // simply remove them 854 | } 855 | else 856 | { 857 | currentLine ~= ch; 858 | } 859 | } 860 | 861 | // last line must have a virtual line-feed, this simplify things 862 | currentLine ~= '\n'; 863 | lines ~= currentLine.dup; 864 | numLine++; 865 | return lines; 866 | } 867 | } 868 | -------------------------------------------------------------------------------- /source/dido/pngload.d: -------------------------------------------------------------------------------- 1 | /// D translation of stb_image-1.33 (http://nothings.org/stb_image.c) 2 | /// 3 | /// This port only supports: 4 | /// $(UL 5 | /// $(LI PNG 8-bit-per-channel only.) 6 | /// ) 7 | /// 8 | //============================ Contributors ========================= 9 | // 10 | // Image formats Optimizations & bugfixes 11 | // Sean Barrett (jpeg, png, bmp) Fabian "ryg" Giesen 12 | // Nicolas Schulz (hdr, psd) 13 | // Jonathan Dummer (tga) Bug fixes & warning fixes 14 | // Jean-Marc Lienher (gif) Marc LeBlanc 15 | // Tom Seddon (pic) Christpher Lloyd 16 | // Thatcher Ulrich (psd) Dave Moore 17 | // Won Chun 18 | // the Horde3D community 19 | // Extensions, features Janez Zemva 20 | // Jetro Lauha (stbi_info) Jonathan Blow 21 | // James "moose2000" Brown (iPhone PNG) Laurent Gomila 22 | // Ben "Disch" Wenger (io callbacks) Aruelien Pocheville 23 | // Martin "SpartanJ" Golini Ryamond Barbiero 24 | // David Woo 25 | module dido.pngload; 26 | 27 | // This has been revived for the sake of loading PNG without too much memory usage. 28 | // It turns out stb_image is more efficient than the loaders using std.zlib. 29 | // https://github.com/lgvz/imageformats/issues/26 30 | 31 | import core.stdc.stdlib; 32 | import core.stdc.string; 33 | 34 | enum STBI_VERSION = 1; 35 | 36 | nothrow: 37 | @nogc: 38 | 39 | enum : int 40 | { 41 | STBI_default = 0, // only used for req_comp 42 | STBI_grey = 1, 43 | STBI_grey_alpha = 2, 44 | STBI_rgb = 3, 45 | STBI_rgb_alpha = 4 46 | }; 47 | 48 | // define faster low-level operations (typically SIMD support) 49 | 50 | // stbi structure is our basic context used by all images, so it 51 | // contains all the IO context, plus some basic image information 52 | struct stbi 53 | { 54 | uint img_x, img_y; 55 | int img_n, img_out_n; 56 | 57 | int buflen; 58 | ubyte[128] buffer_start; 59 | 60 | const(ubyte) *img_buffer; 61 | const(ubyte) *img_buffer_end; 62 | const(ubyte) *img_buffer_original; 63 | } 64 | 65 | 66 | // initialize a memory-decode context 67 | void start_mem(stbi *s, const(ubyte)*buffer, int len) 68 | { 69 | s.img_buffer = buffer; 70 | s.img_buffer_original = buffer; 71 | s.img_buffer_end = buffer+len; 72 | } 73 | 74 | /// Loads an image from memory. 75 | /// Throws: STBImageException on error. 76 | ubyte* stbi_load_png_from_memory(const(void)[] buffer, out int width, out int height, out int components, int requestedComponents) 77 | { 78 | stbi s; 79 | start_mem(&s, cast(const(ubyte)*)buffer.ptr, cast(int)(buffer.length)); 80 | return stbi_png_load(&s, &width, &height, &components, requestedComponents); 81 | } 82 | 83 | 84 | // 85 | // Common code used by all image loaders 86 | // 87 | 88 | enum : int 89 | { 90 | SCAN_load=0, 91 | SCAN_type, 92 | SCAN_header 93 | }; 94 | 95 | 96 | int get8(stbi *s) 97 | { 98 | if (s.img_buffer < s.img_buffer_end) 99 | return *s.img_buffer++; 100 | 101 | return 0; 102 | } 103 | 104 | int at_eof(stbi *s) 105 | { 106 | return s.img_buffer >= s.img_buffer_end; 107 | } 108 | 109 | ubyte get8u(stbi *s) 110 | { 111 | return cast(ubyte) get8(s); 112 | } 113 | 114 | void skip(stbi *s, int n) 115 | { 116 | s.img_buffer += n; 117 | } 118 | 119 | int getn(stbi *s, ubyte *buffer, int n) 120 | { 121 | if (s.img_buffer+n <= s.img_buffer_end) { 122 | memcpy(buffer, s.img_buffer, n); 123 | s.img_buffer += n; 124 | return 1; 125 | } else 126 | return 0; 127 | } 128 | 129 | int get16(stbi *s) 130 | { 131 | int z = get8(s); 132 | return (z << 8) + get8(s); 133 | } 134 | 135 | uint get32(stbi *s) 136 | { 137 | uint z = get16(s); 138 | return (z << 16) + get16(s); 139 | } 140 | 141 | int get16le(stbi *s) 142 | { 143 | int z = get8(s); 144 | return z + (get8(s) << 8); 145 | } 146 | 147 | uint get32le(stbi *s) 148 | { 149 | uint z = get16le(s); 150 | return z + (get16le(s) << 16); 151 | } 152 | 153 | // 154 | // generic converter from built-in img_n to req_comp 155 | // individual types do this automatically as much as possible (e.g. jpeg 156 | // does all cases internally since it needs to colorspace convert anyway, 157 | // and it never has alpha, so very few cases ). png can automatically 158 | // interleave an alpha=255 channel, but falls back to this for other cases 159 | // 160 | // assume data buffer is malloced, so malloc a new one and free that one 161 | // only failure mode is malloc failing 162 | 163 | ubyte compute_y(int r, int g, int b) 164 | { 165 | return cast(ubyte) (((r*77) + (g*150) + (29*b)) >> 8); 166 | } 167 | 168 | ubyte *convert_format(ubyte *data, int img_n, int req_comp, uint x, uint y) 169 | { 170 | int i,j; 171 | ubyte *good; 172 | 173 | if (req_comp == img_n) return data; 174 | assert(req_comp >= 1 && req_comp <= 4); 175 | 176 | good = cast(ubyte*) malloc(req_comp * x * y); 177 | if (good == null) { 178 | free(data); 179 | assert(false); 180 | } 181 | 182 | for (j=0; j < cast(int) y; ++j) { 183 | ubyte *src = data + j * x * img_n ; 184 | ubyte *dest = good + j * x * req_comp; 185 | 186 | // convert source image with img_n components to one with req_comp components; 187 | // avoid switch per pixel, so use switch per scanline and massive macros 188 | switch (img_n * 8 + req_comp) 189 | { 190 | case 1 * 8 + 2: 191 | for(i=x-1; i >= 0; --i, src += 1, dest += 2) 192 | dest[0] = src[0], dest[1] = 255; 193 | break; 194 | case 1 * 8 + 3: 195 | for(i=x-1; i >= 0; --i, src += 1, dest += 3) 196 | dest[0]=dest[1]=dest[2]=src[0]; 197 | break; 198 | case 1 * 8 + 4: 199 | for(i=x-1; i >= 0; --i, src += 1, dest += 4) 200 | dest[0]=dest[1]=dest[2]=src[0], dest[3]=255; 201 | break; 202 | case 2 * 8 + 1: 203 | for(i=x-1; i >= 0; --i, src += 2, dest += 1) 204 | dest[0]=src[0]; 205 | break; 206 | case 2 * 8 + 3: 207 | for(i=x-1; i >= 0; --i, src += 2, dest += 3) 208 | dest[0]=dest[1]=dest[2]=src[0]; 209 | break; 210 | case 2 * 8 + 4: 211 | for(i=x-1; i >= 0; --i, src += 2, dest += 4) 212 | dest[0]=dest[1]=dest[2]=src[0], dest[3]=src[1]; 213 | break; 214 | case 3 * 8 + 4: 215 | for(i=x-1; i >= 0; --i, src += 3, dest += 4) 216 | dest[0]=src[0],dest[1]=src[1],dest[2]=src[2],dest[3]=255; 217 | break; 218 | case 3 * 8 + 1: 219 | for(i=x-1; i >= 0; --i, src += 3, dest += 1) 220 | dest[0]=compute_y(src[0],src[1],src[2]); 221 | break; 222 | case 3 * 8 + 2: 223 | for(i=x-1; i >= 0; --i, src += 3, dest += 2) 224 | dest[0]=compute_y(src[0],src[1],src[2]), dest[1] = 255; 225 | break; 226 | case 4 * 8 + 1: 227 | for(i=x-1; i >= 0; --i, src += 4, dest += 1) 228 | dest[0]=compute_y(src[0],src[1],src[2]); 229 | break; 230 | case 4 * 8 + 2: 231 | for(i=x-1; i >= 0; --i, src += 4, dest += 2) 232 | dest[0]=compute_y(src[0],src[1],src[2]), dest[1] = src[3]; 233 | break; 234 | case 4 * 8 + 3: 235 | for(i=x-1; i >= 0; --i, src += 4, dest += 3) 236 | dest[0]=src[0],dest[1]=src[1],dest[2]=src[2]; 237 | break; 238 | default: assert(0); 239 | } 240 | } 241 | 242 | free(data); 243 | return good; 244 | } 245 | 246 | // public domain zlib decode v0.2 Sean Barrett 2006-11-18 247 | // simple implementation 248 | // - all input must be provided in an upfront buffer 249 | // - all output is written to a single output buffer (can malloc/realloc) 250 | // performance 251 | // - fast huffman 252 | 253 | // fast-way is faster to check than jpeg huffman, but slow way is slower 254 | enum ZFAST_BITS = 9; // accelerate all cases in default tables 255 | enum ZFAST_MASK = ((1 << ZFAST_BITS) - 1); 256 | 257 | // zlib-style huffman encoding 258 | // (jpegs packs from left, zlib from right, so can't share code) 259 | struct zhuffman 260 | { 261 | ushort[1 << ZFAST_BITS] fast; 262 | ushort[16] firstcode; 263 | int[17] maxcode; 264 | ushort[16] firstsymbol; 265 | ubyte[288] size; 266 | ushort[288] value; 267 | } ; 268 | 269 | int bitreverse16(int n) 270 | { 271 | n = ((n & 0xAAAA) >> 1) | ((n & 0x5555) << 1); 272 | n = ((n & 0xCCCC) >> 2) | ((n & 0x3333) << 2); 273 | n = ((n & 0xF0F0) >> 4) | ((n & 0x0F0F) << 4); 274 | n = ((n & 0xFF00) >> 8) | ((n & 0x00FF) << 8); 275 | return n; 276 | } 277 | 278 | int bit_reverse(int v, int bits) 279 | { 280 | assert(bits <= 16); 281 | // to bit reverse n bits, reverse 16 and shift 282 | // e.g. 11 bits, bit reverse and shift away 5 283 | return bitreverse16(v) >> (16-bits); 284 | } 285 | 286 | int zbuild_huffman(zhuffman *z, ubyte *sizelist, int num) 287 | { 288 | int i,k=0; 289 | int code; 290 | int[16] next_code; 291 | int[17] sizes; 292 | 293 | // DEFLATE spec for generating codes 294 | memset(sizes.ptr, 0, sizes.sizeof); 295 | memset(z.fast.ptr, 255, z.fast.sizeof); 296 | for (i=0; i < num; ++i) 297 | ++sizes[sizelist[i]]; 298 | sizes[0] = 0; 299 | for (i=1; i < 16; ++i) 300 | assert(sizes[i] <= (1 << i)); 301 | code = 0; 302 | for (i=1; i < 16; ++i) { 303 | next_code[i] = code; 304 | z.firstcode[i] = cast(ushort) code; 305 | z.firstsymbol[i] = cast(ushort) k; 306 | code = (code + sizes[i]); 307 | if (sizes[i]) 308 | if (code-1 >= (1 << i)) 309 | assert(false); 310 | z.maxcode[i] = code << (16-i); // preshift for inner loop 311 | code <<= 1; 312 | k += sizes[i]; 313 | } 314 | z.maxcode[16] = 0x10000; // sentinel 315 | for (i=0; i < num; ++i) { 316 | int s = sizelist[i]; 317 | if (s) { 318 | int c = next_code[s] - z.firstcode[s] + z.firstsymbol[s]; 319 | z.size[c] = cast(ubyte)s; 320 | z.value[c] = cast(ushort)i; 321 | if (s <= ZFAST_BITS) { 322 | int k_ = bit_reverse(next_code[s],s); 323 | while (k_ < (1 << ZFAST_BITS)) { 324 | z.fast[k_] = cast(ushort) c; 325 | k_ += (1 << s); 326 | } 327 | } 328 | ++next_code[s]; 329 | } 330 | } 331 | return 1; 332 | } 333 | 334 | // zlib-from-memory implementation for PNG reading 335 | // because PNG allows splitting the zlib stream arbitrarily, 336 | // and it's annoying structurally to have PNG call ZLIB call PNG, 337 | // we require PNG read all the IDATs and combine them into a single 338 | // memory buffer 339 | 340 | struct zbuf 341 | { 342 | const(ubyte) *zbuffer; 343 | const(ubyte) *zbuffer_end; 344 | int num_bits; 345 | uint code_buffer; 346 | 347 | ubyte *zout; 348 | ubyte *zout_start; 349 | ubyte *zout_end; 350 | int z_expandable; 351 | 352 | zhuffman z_length, z_distance; 353 | } ; 354 | 355 | int zget8(zbuf *z) 356 | { 357 | if (z.zbuffer >= z.zbuffer_end) return 0; 358 | return *z.zbuffer++; 359 | } 360 | 361 | void fill_bits(zbuf *z) 362 | { 363 | do { 364 | assert(z.code_buffer < (1U << z.num_bits)); 365 | z.code_buffer |= zget8(z) << z.num_bits; 366 | z.num_bits += 8; 367 | } while (z.num_bits <= 24); 368 | } 369 | 370 | uint zreceive(zbuf *z, int n) 371 | { 372 | uint k; 373 | if (z.num_bits < n) fill_bits(z); 374 | k = z.code_buffer & ((1 << n) - 1); 375 | z.code_buffer >>= n; 376 | z.num_bits -= n; 377 | return k; 378 | } 379 | 380 | int zhuffman_decode(zbuf *a, zhuffman *z) 381 | { 382 | int b,s,k; 383 | if (a.num_bits < 16) fill_bits(a); 384 | b = z.fast[a.code_buffer & ZFAST_MASK]; 385 | if (b < 0xffff) { 386 | s = z.size[b]; 387 | a.code_buffer >>= s; 388 | a.num_bits -= s; 389 | return z.value[b]; 390 | } 391 | 392 | // not resolved by fast table, so compute it the slow way 393 | // use jpeg approach, which requires MSbits at top 394 | k = bit_reverse(a.code_buffer, 16); 395 | for (s=ZFAST_BITS+1; ; ++s) 396 | if (k < z.maxcode[s]) 397 | break; 398 | if (s == 16) return -1; // invalid code! 399 | // code size is s, so: 400 | b = (k >> (16-s)) - z.firstcode[s] + z.firstsymbol[s]; 401 | assert(z.size[b] == s); 402 | a.code_buffer >>= s; 403 | a.num_bits -= s; 404 | return z.value[b]; 405 | } 406 | 407 | int expand(zbuf *z, int n) // need to make room for n bytes 408 | { 409 | ubyte *q; 410 | int cur, limit; 411 | if (!z.z_expandable) 412 | assert(false, "Output buffer limit, corrupt PNG"); 413 | cur = cast(int) (z.zout - z.zout_start); 414 | limit = cast(int) (z.zout_end - z.zout_start); 415 | while (cur + n > limit) 416 | limit *= 2; 417 | q = cast(ubyte*) realloc(z.zout_start, limit); 418 | if (q == null) 419 | assert(false, "Out of memory"); 420 | z.zout_start = q; 421 | z.zout = q + cur; 422 | z.zout_end = q + limit; 423 | return 1; 424 | } 425 | 426 | static immutable int[31] length_base = [ 427 | 3,4,5,6,7,8,9,10,11,13, 428 | 15,17,19,23,27,31,35,43,51,59, 429 | 67,83,99,115,131,163,195,227,258,0,0 ]; 430 | 431 | static immutable int[31] length_extra = 432 | [ 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0 ]; 433 | 434 | static immutable int[32] dist_base = [ 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193, 435 | 257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0]; 436 | 437 | static immutable int[32] dist_extra = 438 | [ 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13]; 439 | 440 | int parse_huffman_block(zbuf *a) 441 | { 442 | for(;;) { 443 | int z = zhuffman_decode(a, &a.z_length); 444 | if (z < 256) { 445 | if (z < 0) 446 | assert(false, "Bad Huffman code, corrupt PNG"); 447 | if (a.zout >= a.zout_end) if (!expand(a, 1)) return 0; 448 | *a.zout++ = cast(ubyte) z; 449 | } else { 450 | ubyte *p; 451 | int len,dist; 452 | if (z == 256) return 1; 453 | z -= 257; 454 | len = length_base[z]; 455 | if (length_extra[z]) len += zreceive(a, length_extra[z]); 456 | z = zhuffman_decode(a, &a.z_distance); 457 | if (z < 0) assert(false, "Bad Huffman code, corrupt PNG"); 458 | dist = dist_base[z]; 459 | if (dist_extra[z]) dist += zreceive(a, dist_extra[z]); 460 | if (a.zout - a.zout_start < dist) assert(false, "Bad dist, corrupt PNG"); 461 | if (a.zout + len > a.zout_end) if (!expand(a, len)) return 0; 462 | p = a.zout - dist; 463 | while (len--) 464 | *a.zout++ = *p++; 465 | } 466 | } 467 | } 468 | 469 | int compute_huffman_codes(zbuf *a) 470 | { 471 | static immutable ubyte[19] length_dezigzag = [ 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 ]; 472 | zhuffman z_codelength; 473 | ubyte[286+32+137] lencodes;//padding for maximum single op 474 | ubyte[19] codelength_sizes; 475 | int i,n; 476 | 477 | int hlit = zreceive(a,5) + 257; 478 | int hdist = zreceive(a,5) + 1; 479 | int hclen = zreceive(a,4) + 4; 480 | 481 | memset(codelength_sizes.ptr, 0, codelength_sizes.sizeof); 482 | for (i=0; i < hclen; ++i) { 483 | int s = zreceive(a,3); 484 | codelength_sizes[length_dezigzag[i]] = cast(ubyte) s; 485 | } 486 | if (!zbuild_huffman(&z_codelength, codelength_sizes.ptr, 19)) return 0; 487 | 488 | n = 0; 489 | while (n < hlit + hdist) { 490 | int c = zhuffman_decode(a, &z_codelength); 491 | assert(c >= 0 && c < 19); 492 | if (c < 16) 493 | lencodes[n++] = cast(ubyte) c; 494 | else if (c == 16) { 495 | c = zreceive(a,2)+3; 496 | memset(lencodes.ptr+n, lencodes[n-1], c); 497 | n += c; 498 | } else if (c == 17) { 499 | c = zreceive(a,3)+3; 500 | memset(lencodes.ptr+n, 0, c); 501 | n += c; 502 | } else { 503 | assert(c == 18); 504 | c = zreceive(a,7)+11; 505 | memset(lencodes.ptr+n, 0, c); 506 | n += c; 507 | } 508 | } 509 | if (n != hlit+hdist) assert(false, "Bad codelengths, corrupt PNG"); 510 | if (!zbuild_huffman(&a.z_length, lencodes.ptr, hlit)) return 0; 511 | if (!zbuild_huffman(&a.z_distance, lencodes.ptr+hlit, hdist)) return 0; 512 | return 1; 513 | } 514 | 515 | int parse_uncompressed_block(zbuf *a) 516 | { 517 | ubyte[4] header; 518 | int len,nlen,k; 519 | if (a.num_bits & 7) 520 | zreceive(a, a.num_bits & 7); // discard 521 | // drain the bit-packed data into header 522 | k = 0; 523 | while (a.num_bits > 0) { 524 | header[k++] = cast(ubyte) (a.code_buffer & 255); // wtf this warns? 525 | a.code_buffer >>= 8; 526 | a.num_bits -= 8; 527 | } 528 | assert(a.num_bits == 0); 529 | // now fill header the normal way 530 | while (k < 4) 531 | header[k++] = cast(ubyte) zget8(a); 532 | len = header[1] * 256 + header[0]; 533 | nlen = header[3] * 256 + header[2]; 534 | if (nlen != (len ^ 0xffff)) assert(false, "Zlib corrupt, corrupt PNG"); 535 | if (a.zbuffer + len > a.zbuffer_end) assert(false, "Read past buffer, corrupt PNG"); 536 | if (a.zout + len > a.zout_end) 537 | if (!expand(a, len)) return 0; 538 | memcpy(a.zout, a.zbuffer, len); 539 | a.zbuffer += len; 540 | a.zout += len; 541 | return 1; 542 | } 543 | 544 | int parse_zlib_header(zbuf *a) 545 | { 546 | int cmf = zget8(a); 547 | int cm = cmf & 15; 548 | /* int cinfo = cmf >> 4; */ 549 | int flg = zget8(a); 550 | if ((cmf*256+flg) % 31 != 0) assert(false, "Bad zlib header, corrupt PNG"); // zlib spec 551 | if (flg & 32) assert(false, "No preset dict, corrupt PNG"); // preset dictionary not allowed in png 552 | if (cm != 8) assert(false, "Bad compression, corrupt PNG"); // DEFLATE required for png 553 | // window = 1 << (8 + cinfo)... but who cares, we fully buffer output 554 | return 1; 555 | } 556 | 557 | // MAYDO: should statically initialize these for optimal thread safety 558 | __gshared ubyte[288] default_length; 559 | __gshared ubyte[32] default_distance; 560 | 561 | void init_defaults() 562 | { 563 | int i; // use <= to match clearly with spec 564 | for (i=0; i <= 143; ++i) default_length[i] = 8; 565 | for ( ; i <= 255; ++i) default_length[i] = 9; 566 | for ( ; i <= 279; ++i) default_length[i] = 7; 567 | for ( ; i <= 287; ++i) default_length[i] = 8; 568 | 569 | for (i=0; i <= 31; ++i) default_distance[i] = 5; 570 | } 571 | 572 | __gshared int stbi_png_partial; // a quick hack to only allow decoding some of a PNG... I should implement real streaming support instead 573 | int parse_zlib(zbuf *a, int parse_header) 574 | { 575 | int final_, type; 576 | if (parse_header) 577 | if (!parse_zlib_header(a)) return 0; 578 | a.num_bits = 0; 579 | a.code_buffer = 0; 580 | do { 581 | final_ = zreceive(a,1); 582 | type = zreceive(a,2); 583 | if (type == 0) { 584 | if (!parse_uncompressed_block(a)) return 0; 585 | } else if (type == 3) { 586 | return 0; 587 | } else { 588 | if (type == 1) { 589 | // use fixed code lengths 590 | if (!default_distance[31]) init_defaults(); 591 | if (!zbuild_huffman(&a.z_length , default_length.ptr , 288)) return 0; 592 | if (!zbuild_huffman(&a.z_distance, default_distance.ptr, 32)) return 0; 593 | } else { 594 | if (!compute_huffman_codes(a)) return 0; 595 | } 596 | if (!parse_huffman_block(a)) return 0; 597 | } 598 | if (stbi_png_partial && a.zout - a.zout_start > 65536) 599 | break; 600 | } while (!final_); 601 | return 1; 602 | } 603 | 604 | int do_zlib(zbuf *a, ubyte *obuf, int olen, int exp, int parse_header) 605 | { 606 | a.zout_start = obuf; 607 | a.zout = obuf; 608 | a.zout_end = obuf + olen; 609 | a.z_expandable = exp; 610 | 611 | return parse_zlib(a, parse_header); 612 | } 613 | 614 | ubyte *stbi_zlib_decode_malloc_guesssize(const(ubyte) *buffer, int len, int initial_size, int *outlen) 615 | { 616 | zbuf a; 617 | ubyte *p = cast(ubyte*) malloc(initial_size); 618 | if (p == null) return null; 619 | a.zbuffer = buffer; 620 | a.zbuffer_end = buffer + len; 621 | if (do_zlib(&a, p, initial_size, 1, 1)) { 622 | if (outlen) *outlen = cast(int) (a.zout - a.zout_start); 623 | return a.zout_start; 624 | } else { 625 | free(a.zout_start); 626 | return null; 627 | } 628 | } 629 | 630 | ubyte *stbi_zlib_decode_malloc(const(ubyte) *buffer, int len, int *outlen) 631 | { 632 | return stbi_zlib_decode_malloc_guesssize(buffer, len, 16384, outlen); 633 | } 634 | 635 | ubyte *stbi_zlib_decode_malloc_guesssize_headerflag(const(ubyte) *buffer, int len, int initial_size, int *outlen, int parse_header) 636 | { 637 | zbuf a; 638 | ubyte *p = cast(ubyte*) malloc(initial_size); 639 | if (p == null) return null; 640 | a.zbuffer = buffer; 641 | a.zbuffer_end = buffer + len; 642 | if (do_zlib(&a, p, initial_size, 1, parse_header)) { 643 | if (outlen) *outlen = cast(int) (a.zout - a.zout_start); 644 | return a.zout_start; 645 | } else { 646 | free(a.zout_start); 647 | return null; 648 | } 649 | } 650 | 651 | int stbi_zlib_decode_buffer(ubyte* obuffer, int olen, const(ubyte)* ibuffer, int ilen) 652 | { 653 | zbuf a; 654 | a.zbuffer = ibuffer; 655 | a.zbuffer_end = ibuffer + ilen; 656 | if (do_zlib(&a, obuffer, olen, 0, 1)) 657 | return cast(int) (a.zout - a.zout_start); 658 | else 659 | return -1; 660 | } 661 | 662 | ubyte *stbi_zlib_decode_noheader_malloc(const(ubyte) *buffer, int len, int *outlen) 663 | { 664 | zbuf a; 665 | ubyte *p = cast(ubyte*) malloc(16384); 666 | if (p == null) return null; 667 | a.zbuffer = buffer; 668 | a.zbuffer_end = buffer+len; 669 | if (do_zlib(&a, p, 16384, 1, 0)) { 670 | if (outlen) *outlen = cast(int) (a.zout - a.zout_start); 671 | return a.zout_start; 672 | } else { 673 | free(a.zout_start); 674 | return null; 675 | } 676 | } 677 | 678 | int stbi_zlib_decode_noheader_buffer(ubyte *obuffer, int olen, const(ubyte) *ibuffer, int ilen) 679 | { 680 | zbuf a; 681 | a.zbuffer = ibuffer; 682 | a.zbuffer_end = ibuffer + ilen; 683 | if (do_zlib(&a, obuffer, olen, 0, 0)) 684 | return cast(int) (a.zout - a.zout_start); 685 | else 686 | return -1; 687 | } 688 | 689 | // public domain "baseline" PNG decoder v0.10 Sean Barrett 2006-11-18 690 | // simple implementation 691 | // - only 8-bit samples 692 | // - no CRC checking 693 | // - allocates lots of intermediate memory 694 | // - avoids problem of streaming data between subsystems 695 | // - avoids explicit window management 696 | // performance 697 | // - uses stb_zlib, a PD zlib implementation with fast huffman decoding 698 | 699 | 700 | struct chunk 701 | { 702 | uint length; 703 | uint type; 704 | } 705 | 706 | uint PNG_TYPE(ubyte a, ubyte b, ubyte c, ubyte d) 707 | { 708 | return (a << 24) + (b << 16) + (c << 8) + d; 709 | } 710 | 711 | chunk get_chunk_header(stbi *s) 712 | { 713 | chunk c; 714 | c.length = get32(s); 715 | c.type = get32(s); 716 | return c; 717 | } 718 | 719 | static int check_png_header(stbi *s) 720 | { 721 | static immutable ubyte[8] png_sig = [ 137, 80, 78, 71, 13, 10, 26, 10 ]; 722 | for (int i = 0; i < 8; ++i) 723 | { 724 | ubyte headerByte = get8u(s); 725 | ubyte expected = png_sig[i]; 726 | if (headerByte != expected) 727 | assert(false, "Bad PNG sig, not a PNG"); 728 | } 729 | return 1; 730 | } 731 | 732 | struct png 733 | { 734 | stbi *s; 735 | ubyte *idata; 736 | ubyte *expanded; 737 | ubyte *out_; 738 | } 739 | 740 | 741 | enum : int 742 | { 743 | F_none=0, F_sub=1, F_up=2, F_avg=3, F_paeth=4, 744 | F_avg_first, F_paeth_first 745 | } 746 | 747 | static immutable ubyte[5] first_row_filter = 748 | [ 749 | F_none, F_sub, F_none, F_avg_first, F_paeth_first 750 | ]; 751 | 752 | static int paeth(int a, int b, int c) 753 | { 754 | int p = a + b - c; 755 | int pa = abs(p-a); 756 | int pb = abs(p-b); 757 | int pc = abs(p-c); 758 | if (pa <= pb && pa <= pc) return a; 759 | if (pb <= pc) return b; 760 | return c; 761 | } 762 | 763 | // create the png data from post-deflated data 764 | static int create_png_image_raw(png *a, ubyte *raw, uint raw_len, int out_n, uint x, uint y) 765 | { 766 | stbi *s = a.s; 767 | uint i,j,stride = x*out_n; 768 | int k; 769 | int img_n = s.img_n; // copy it into a local for later 770 | assert(out_n == s.img_n || out_n == s.img_n+1); 771 | if (stbi_png_partial) y = 1; 772 | a.out_ = cast(ubyte*) malloc(x * y * out_n); 773 | if (!a.out_) assert(false, "Out of memory"); 774 | if (!stbi_png_partial) { 775 | if (s.img_x == x && s.img_y == y) { 776 | if (raw_len != (img_n * x + 1) * y) assert(false, "Not enough pixels, corrupt PNG"); 777 | } else { // interlaced: 778 | if (raw_len < (img_n * x + 1) * y) assert(false, "Not enough pixels, corrupt PNG"); 779 | } 780 | } 781 | for (j=0; j < y; ++j) { 782 | ubyte *cur = a.out_ + stride*j; 783 | ubyte *prior = cur - stride; 784 | int filter = *raw++; 785 | if (filter > 4) assert(false, "Invalid filter, corrupt PNG"); 786 | // if first row, use special filter that doesn't sample previous row 787 | if (j == 0) filter = first_row_filter[filter]; 788 | // handle first pixel explicitly 789 | for (k=0; k < img_n; ++k) { 790 | switch (filter) { 791 | case F_none : cur[k] = raw[k]; break; 792 | case F_sub : cur[k] = raw[k]; break; 793 | case F_up : cur[k] = cast(ubyte)(raw[k] + prior[k]); break; 794 | case F_avg : cur[k] = cast(ubyte)(raw[k] + (prior[k]>>1)); break; 795 | case F_paeth : cur[k] = cast(ubyte) (raw[k] + paeth(0,prior[k],0)); break; 796 | case F_avg_first : cur[k] = raw[k]; break; 797 | case F_paeth_first: cur[k] = raw[k]; break; 798 | default: break; 799 | } 800 | } 801 | if (img_n != out_n) cur[img_n] = 255; 802 | raw += img_n; 803 | cur += out_n; 804 | prior += out_n; 805 | // this is a little gross, so that we don't switch per-pixel or per-component 806 | if (img_n == out_n) { 807 | 808 | for (i=x-1; i >= 1; --i, raw+=img_n,cur+=img_n,prior+=img_n) 809 | for (k=0; k < img_n; ++k) 810 | { 811 | switch (filter) { 812 | case F_none: cur[k] = raw[k]; break; 813 | case F_sub: cur[k] = cast(ubyte)(raw[k] + cur[k-img_n]); break; 814 | case F_up: cur[k] = cast(ubyte)(raw[k] + prior[k]); break; 815 | case F_avg: cur[k] = cast(ubyte)(raw[k] + ((prior[k] + cur[k-img_n])>>1)); break; 816 | case F_paeth: cur[k] = cast(ubyte) (raw[k] + paeth(cur[k-img_n],prior[k],prior[k-img_n])); break; 817 | case F_avg_first: cur[k] = cast(ubyte)(raw[k] + (cur[k-img_n] >> 1)); break; 818 | case F_paeth_first: cur[k] = cast(ubyte) (raw[k] + paeth(cur[k-img_n],0,0)); break; 819 | default: break; 820 | } 821 | } 822 | } else { 823 | assert(img_n+1 == out_n); 824 | 825 | for (i=x-1; i >= 1; --i, cur[img_n]=255,raw+=img_n,cur+=out_n,prior+=out_n) 826 | for (k=0; k < img_n; ++k) 827 | { 828 | switch (filter) { 829 | case F_none: cur[k] = raw[k]; break; 830 | case F_sub: cur[k] = cast(ubyte)(raw[k] + cur[k-out_n]); break; 831 | case F_up: cur[k] = cast(ubyte)(raw[k] + prior[k]); break; 832 | case F_avg: cur[k] = cast(ubyte)(raw[k] + ((prior[k] + cur[k-out_n])>>1)); break; 833 | case F_paeth: cur[k] = cast(ubyte) (raw[k] + paeth(cur[k-out_n],prior[k],prior[k-out_n])); break; 834 | case F_avg_first: cur[k] = cast(ubyte)(raw[k] + (cur[k-out_n] >> 1)); break; 835 | case F_paeth_first: cur[k] = cast(ubyte) (raw[k] + paeth(cur[k-out_n],0,0)); break; 836 | default: break; 837 | } 838 | } 839 | } 840 | } 841 | return 1; 842 | } 843 | 844 | int create_png_image(png *a, ubyte *raw, uint raw_len, int out_n, int interlaced) 845 | { 846 | ubyte *final_; 847 | int p; 848 | int save; 849 | if (!interlaced) 850 | return create_png_image_raw(a, raw, raw_len, out_n, a.s.img_x, a.s.img_y); 851 | save = stbi_png_partial; 852 | stbi_png_partial = 0; 853 | 854 | // de-interlacing 855 | final_ = cast(ubyte*) malloc(a.s.img_x * a.s.img_y * out_n); 856 | for (p=0; p < 7; ++p) { 857 | static immutable int[7] xorig = [ 0,4,0,2,0,1,0 ]; 858 | static immutable int[7] yorig = [ 0,0,4,0,2,0,1 ]; 859 | static immutable int[7] xspc = [ 8,8,4,4,2,2,1 ]; 860 | static immutable int[7] yspc = [ 8,8,8,4,4,2,2 ]; 861 | int i,j,x,y; 862 | // pass1_x[4] = 0, pass1_x[5] = 1, pass1_x[12] = 1 863 | x = (a.s.img_x - xorig[p] + xspc[p]-1) / xspc[p]; 864 | y = (a.s.img_y - yorig[p] + yspc[p]-1) / yspc[p]; 865 | if (x && y) { 866 | if (!create_png_image_raw(a, raw, raw_len, out_n, x, y)) { 867 | free(final_); 868 | return 0; 869 | } 870 | for (j=0; j < y; ++j) 871 | for (i=0; i < x; ++i) 872 | memcpy(final_ + (j*yspc[p]+yorig[p])*a.s.img_x*out_n + (i*xspc[p]+xorig[p])*out_n, 873 | a.out_ + (j*x+i)*out_n, out_n); 874 | free(a.out_); 875 | raw += (x*out_n+1)*y; 876 | raw_len -= (x*out_n+1)*y; 877 | } 878 | } 879 | a.out_ = final_; 880 | 881 | stbi_png_partial = save; 882 | return 1; 883 | } 884 | 885 | static int compute_transparency(png *z, ubyte[3] tc, int out_n) 886 | { 887 | stbi *s = z.s; 888 | uint i, pixel_count = s.img_x * s.img_y; 889 | ubyte *p = z.out_; 890 | 891 | // compute color-based transparency, assuming we've 892 | // already got 255 as the alpha value in the output 893 | assert(out_n == 2 || out_n == 4); 894 | 895 | if (out_n == 2) { 896 | for (i=0; i < pixel_count; ++i) { 897 | p[1] = (p[0] == tc[0] ? 0 : 255); 898 | p += 2; 899 | } 900 | } else { 901 | for (i=0; i < pixel_count; ++i) { 902 | if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2]) 903 | p[3] = 0; 904 | p += 4; 905 | } 906 | } 907 | return 1; 908 | } 909 | 910 | int expand_palette(png *a, ubyte *palette, int len, int pal_img_n) 911 | { 912 | uint i, pixel_count = a.s.img_x * a.s.img_y; 913 | ubyte *p; 914 | ubyte *temp_out; 915 | ubyte *orig = a.out_; 916 | 917 | p = cast(ubyte*) malloc(pixel_count * pal_img_n); 918 | if (p == null) 919 | assert(false, "Out of memory"); 920 | 921 | // between here and free(out) below, exitting would leak 922 | temp_out = p; 923 | 924 | if (pal_img_n == 3) { 925 | for (i=0; i < pixel_count; ++i) { 926 | int n = orig[i]*4; 927 | p[0] = palette[n ]; 928 | p[1] = palette[n+1]; 929 | p[2] = palette[n+2]; 930 | p += 3; 931 | } 932 | } else { 933 | for (i=0; i < pixel_count; ++i) { 934 | int n = orig[i]*4; 935 | p[0] = palette[n ]; 936 | p[1] = palette[n+1]; 937 | p[2] = palette[n+2]; 938 | p[3] = palette[n+3]; 939 | p += 4; 940 | } 941 | } 942 | free(a.out_); 943 | a.out_ = temp_out; 944 | 945 | return 1; 946 | } 947 | 948 | int parse_png_file(png *z, int scan, int req_comp) 949 | { 950 | ubyte[1024] palette; 951 | ubyte pal_img_n=0; 952 | ubyte has_trans=0; 953 | ubyte[3] tc; 954 | uint ioff=0, idata_limit=0, i, pal_len=0; 955 | int first=1,k,interlace=0; 956 | stbi *s = z.s; 957 | 958 | z.expanded = null; 959 | z.idata = null; 960 | z.out_ = null; 961 | 962 | if (!check_png_header(s)) return 0; 963 | 964 | if (scan == SCAN_type) return 1; 965 | 966 | for (;;) { 967 | chunk c = get_chunk_header(s); 968 | switch (c.type) { 969 | case PNG_TYPE('I','H','D','R'): { 970 | int depth,color,comp,filter; 971 | if (!first) assert(false, "Multiple IHDR, corrupt PNG"); 972 | first = 0; 973 | if (c.length != 13) assert(false, "Bad IHDR len, corrupt PNG"); 974 | s.img_x = get32(s); if (s.img_x > (1 << 24)) assert(false, "Very large image (corrupt?)"); 975 | s.img_y = get32(s); if (s.img_y > (1 << 24)) assert(false, "Very large image (corrupt?)"); 976 | depth = get8(s); if (depth != 8) assert(false, "8bit only, PNG not supported: 8-bit only"); 977 | color = get8(s); if (color > 6) assert(false, "Bad ctype, corrupt PNG"); 978 | if (color == 3) pal_img_n = 3; else if (color & 1) assert(false, "Bad ctype, corrupt PNG"); 979 | comp = get8(s); if (comp) assert(false, "Bad comp method, corrupt PNG"); 980 | filter= get8(s); if (filter) assert(false, "Bad filter method, corrupt PNG"); 981 | interlace = get8(s); if (interlace>1) assert(false, "Bad interlace method, corrupt PNG"); 982 | if (!s.img_x || !s.img_y) assert(false, "0-pixel image, corrupt PNG"); 983 | if (!pal_img_n) { 984 | s.img_n = (color & 2 ? 3 : 1) + (color & 4 ? 1 : 0); 985 | if ((1 << 30) / s.img_x / s.img_n < s.img_y) assert(false, "Image too large to decode"); 986 | if (scan == SCAN_header) return 1; 987 | } else { 988 | // if paletted, then pal_n is our final components, and 989 | // img_n is # components to decompress/filter. 990 | s.img_n = 1; 991 | if ((1 << 30) / s.img_x / 4 < s.img_y) assert(false, "Too large, corrupt PNG"); 992 | // if SCAN_header, have to scan to see if we have a tRNS 993 | } 994 | break; 995 | } 996 | 997 | case PNG_TYPE('P','L','T','E'): { 998 | if (first) assert(false, "first not IHDR, corrupt PNG"); 999 | if (c.length > 256*3) assert(false, "invalid PLTE, corrupt PNG"); 1000 | pal_len = c.length / 3; 1001 | if (pal_len * 3 != c.length) assert(false, "invalid PLTE, corrupt PNG"); 1002 | for (i=0; i < pal_len; ++i) { 1003 | palette[i*4+0] = get8u(s); 1004 | palette[i*4+1] = get8u(s); 1005 | palette[i*4+2] = get8u(s); 1006 | palette[i*4+3] = 255; 1007 | } 1008 | break; 1009 | } 1010 | 1011 | case PNG_TYPE('t','R','N','S'): { 1012 | if (first) assert(false, "first not IHDR, cCorrupt PNG"); 1013 | if (z.idata) assert(false, "tRNS after IDAT, corrupt PNG"); 1014 | if (pal_img_n) { 1015 | if (scan == SCAN_header) { s.img_n = 4; return 1; } 1016 | if (pal_len == 0) assert(false, "tRNS before PLTE, corrupt PNG"); 1017 | if (c.length > pal_len) assert(false, "bad tRNS len, corrupt PNG"); 1018 | pal_img_n = 4; 1019 | for (i=0; i < c.length; ++i) 1020 | palette[i*4+3] = get8u(s); 1021 | } else { 1022 | if (!(s.img_n & 1)) assert(false, "tRNS with alpha, corrupt PNG"); 1023 | if (c.length != cast(uint) s.img_n*2) assert(false, "bad tRNS len, corrupt PNG"); 1024 | has_trans = 1; 1025 | for (k=0; k < s.img_n; ++k) 1026 | tc[k] = cast(ubyte) get16(s); // non 8-bit images will be larger 1027 | } 1028 | break; 1029 | } 1030 | 1031 | case PNG_TYPE('I','D','A','T'): { 1032 | if (first) assert(false, "first not IHDR, corrupt PNG"); 1033 | if (pal_img_n && !pal_len) assert(false, "no PLTE, corrupt PNG"); 1034 | if (scan == SCAN_header) { s.img_n = pal_img_n; return 1; } 1035 | if (ioff + c.length > idata_limit) { 1036 | ubyte *p; 1037 | if (idata_limit == 0) idata_limit = c.length > 4096 ? c.length : 4096; 1038 | while (ioff + c.length > idata_limit) 1039 | idata_limit *= 2; 1040 | p = cast(ubyte*) realloc(z.idata, idata_limit); if (p == null) assert(false, "outofmem, cOut of memory"); 1041 | z.idata = p; 1042 | } 1043 | if (!getn(s, z.idata+ioff,c.length)) assert(false, "outofdata, corrupt PNG"); 1044 | ioff += c.length; 1045 | break; 1046 | } 1047 | 1048 | case PNG_TYPE('I','E','N','D'): { 1049 | uint raw_len; 1050 | if (first) assert(false, "first not IHDR, corrupt PNG"); 1051 | if (scan != SCAN_load) return 1; 1052 | if (z.idata == null) assert(false, "no IDAT, corrupt PNG"); 1053 | z.expanded = stbi_zlib_decode_malloc_guesssize_headerflag(z.idata, ioff, 16384, cast(int *) &raw_len, 1); 1054 | if (z.expanded == null) return 0; // zlib should set error 1055 | free(z.idata); z.idata = null; 1056 | if ((req_comp == s.img_n+1 && req_comp != 3 && !pal_img_n) || has_trans) 1057 | s.img_out_n = s.img_n+1; 1058 | else 1059 | s.img_out_n = s.img_n; 1060 | if (!create_png_image(z, z.expanded, raw_len, s.img_out_n, interlace)) return 0; 1061 | if (has_trans) 1062 | if (!compute_transparency(z, tc, s.img_out_n)) return 0; 1063 | if (pal_img_n) { 1064 | // pal_img_n == 3 or 4 1065 | s.img_n = pal_img_n; // record the actual colors we had 1066 | s.img_out_n = pal_img_n; 1067 | if (req_comp >= 3) s.img_out_n = req_comp; 1068 | if (!expand_palette(z, palette.ptr, pal_len, s.img_out_n)) 1069 | return 0; 1070 | } 1071 | free(z.expanded); z.expanded = null; 1072 | return 1; 1073 | } 1074 | 1075 | default: 1076 | // if critical, fail 1077 | if (first) assert(false, "first not IHDR, corrupt PNG"); 1078 | if ((c.type & (1 << 29)) == 0) { 1079 | assert(false, "PNG not supported: unknown chunk type"); 1080 | } 1081 | skip(s, c.length); 1082 | break; 1083 | } 1084 | // end of chunk, read and skip CRC 1085 | get32(s); 1086 | } 1087 | } 1088 | 1089 | ubyte *do_png(png *p, int *x, int *y, int *n, int req_comp) 1090 | { 1091 | ubyte *result=null; 1092 | if (req_comp < 0 || req_comp > 4) 1093 | assert(false, "Internal error: bad req_comp"); 1094 | if (parse_png_file(p, SCAN_load, req_comp)) { 1095 | result = p.out_; 1096 | p.out_ = null; 1097 | if (req_comp && req_comp != p.s.img_out_n) { 1098 | result = convert_format(result, p.s.img_out_n, req_comp, p.s.img_x, p.s.img_y); 1099 | p.s.img_out_n = req_comp; 1100 | if (result == null) return result; 1101 | } 1102 | *x = p.s.img_x; 1103 | *y = p.s.img_y; 1104 | if (n) *n = p.s.img_n; 1105 | } 1106 | free(p.out_); p.out_ = null; 1107 | free(p.expanded); p.expanded = null; 1108 | free(p.idata); p.idata = null; 1109 | 1110 | return result; 1111 | } 1112 | 1113 | ubyte *stbi_png_load(stbi *s, int *x, int *y, int *comp, int req_comp) 1114 | { 1115 | png p; 1116 | p.s = s; 1117 | return do_png(&p, x,y,comp,req_comp); 1118 | } 1119 | --------------------------------------------------------------------------------