import arsd.minigui; import arsd.textlayouter; import arsd.core; import arsd.ini; import std.string; import std.regex; import std.process : environment; import std.path : stripExtension, isValidPath, buildPath, baseName, dirName, extension; import std.file; import std.stdio; import std.conv; import std.algorithm; import std.array; string openFilePath = null; Rule[] rules; string[] syntaxFiles; string defaultConfig = q{[colors] bg=16, 16, 16 fg=245, 245, 245 selbg=0, 120, 255 selfg=245, 245, 245 }; string defaultDSyntax = q{[highlight] `\b(if|else|for|while|switch|case|break|continue|return|import|module|class|struct|enum|template)\b`=220, 120, 20 `\b(int|float|double|real|char|wchar|dchar|bool|void|string)\b`=80, 160, 255 `\b(true|false|null|__FILE__|__LINE__)\b`=200, 200, 120 `==|!=|<=|>=|&&|\|\||[+\-*/%=<>!]`=255, 100, 100 `[()\[\]{}]`=200, 200, 200 `[;:,.]`=200, 200, 200 `\b\d+\b`=180, 140, 255 `"([^"\\]|\\.)*"`=100, 200, 120 `//.*`=120, 120, 120 }; void refreshHighlight(string sfdir) { rules.length = 0; syntaxFiles.length = 0; foreach (string name; dirEntries(sfdir, SpanMode.breadth)) { syntaxFiles ~= name; if (!openFilePath.length) continue; auto openExt = extension(openFilePath); if (openExt == "." ~ baseName(name).stripExtension()) { string[] syntaxFile = readText(name).split("\n"); Rule[] newRules; foreach (line; syntaxFile) { if (line.length == 0 || line[0] != '`') continue; auto pos = line.lastIndexOf('='); if (pos == -1) continue; string before = line[1 .. pos-1]; string after = line[pos + 1 .. $]; string[] rgb = after .split(",") .map!(s => s.replace(" ", "")) .array; newRules ~= Rule( regex(before), Color(rgb[0].to!int, rgb[1].to!int, rgb[2].to!int) ); } rules = newRules; } } } string ensureFile(string path, string def_contents) { if (exists(path)) return readText(path); auto dir = dirName(path); if (dir.length > 0) mkdirRecurse(dir); std.file.write(path, def_contents); return def_contents; } string getConfigPath(string appName, string fileName) { version (Windows) { auto base = environment.get("APPDATA"); return buildPath(base, appName, fileName); } else version (OSX) { auto base = environment.get("HOME"); return buildPath(base, "Library/Application Support", appName, fileName); } else { auto base = environment.get("XDG_CONFIG_HOME", environment.get("HOME") ~ "/.config"); return buildPath(base, appName, fileName); } } class MyTheme : VisualTheme!MyTheme { mixin DefaultDarkTheme; override Color selectionBackgroundColor() { return Color(0, 120, 255); } override Color selectionForegroundColor() { return Color(245, 245, 245); } } struct Rule { Regex!char re; Color color; } class AnimatedTextEdit : CustomTextEdit { this(Widget parent) { super(parent); } static class Style : CustomTextEdit.Style { override OperatingSystemFont font() { return new OperatingSystemFont("monospace", 24); } override WidgetBackground background() { return WidgetBackground(Color(16, 16, 16)); } override Color foregroundColor() { return Color(245, 245, 245); } override FrameStyle borderStyle() { return FrameStyle.none; } } mixin OverrideStyle!Style; string searching; override TextDisplayHelper textDisplayHelperFactory( TextLayouter textLayout, ScrollMessageWidget smw ) { smw.verticalScrollBar.hide(); smw.horizontalScrollBar.hide(); return new MyTextDisplayHelper(this, textLayout, smw); } static class MyTextDisplayHelper : TextDisplayHelper { AnimatedTextEdit owner; this(AnimatedTextEdit owner, TextLayouter textLayout, ScrollMessageWidget smw) { this.owner = owner; super(textLayout, smw); } float camX = 0; float camY = 0; override void drawTextSegment( MyTextStyle ignoredGenericStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text ) { auto t = text.idup; Color defaultCol = getComputedStyle().foregroundColor; if (owner.searching.length && t.indexOf(owner.searching) != -1) { painter.fillColor = Color(40, 40, 40); painter.outlineColor = Color(80, 80, 80); painter.drawRectangle(upperLeft, painter.textSize(text)); } auto pos = upperLeft; size_t i = 0; while (i < text.length) { size_t bestIdx = size_t.max; string bestMatch; Color bestColor; foreach (rule; rules) { auto m = matchFirst(t[i .. $], rule.re); if (!m.empty) { auto idx = i + m.pre.length; if (idx < bestIdx) { bestIdx = idx; bestMatch = m.hit.idup; bestColor = rule.color; } } } if (bestIdx == size_t.max) { painter.outlineColor = defaultCol; painter.drawText(pos, text[i .. $]); break; } if (bestIdx > i) { auto normalPart = text[i .. bestIdx]; painter.outlineColor = defaultCol; painter.drawText(pos, normalPart); pos.x += ignoredGenericStyle.font.stringWidth(normalPart); } painter.outlineColor = bestColor; painter.drawText(pos, bestMatch); pos.x += ignoredGenericStyle.font.stringWidth(bestMatch); i = bestIdx + bestMatch.length; } } override Rectangle paintContent( WidgetPainter painter, const Rectangle bounds ) { TextLayouter.CaretInformation ci; TextStyle ts; l.getDrawableText(delegate bool(txt, style, info, carets...) { ts = style; foreach (caret; carets) ci = caret; return true; }); float targetX = this.width / 2 - ci.boundingBox.center.x; float targetY = this.height / 2 - ci.boundingBox.center.y; float smoothing = 0.15; camX += (targetX - camX) * smoothing; camY += (targetY - camY) * smoothing; auto baseX = painter.originX; auto baseY = painter.originY; painter.originX = baseX + cast(int)camX; painter.originY = baseY + cast(int)camY; auto result = super.paintContent(painter, bounds); auto cs = getComputedStyle(); auto fg = cs.foregroundColor; auto caretBox = ci.boundingBox; auto block = Rectangle( Point( (caretBox.upperLeft + bounds.upperLeft - smw.position()).x, (caretBox.upperLeft + bounds.upperLeft - smw.position()).y - 1 ), Size( cast(int)ts.font().averageWidth(), cast(int)ts.font().height + 2 ) ); painter.fillColor = Color(0, 0, 0, 0); painter.outlineColor = fg; painter.drawRectangle(block); painter.notifyCursorPosition( block.left, block.top, block.width, block.height ); return result; } } } struct ConfigPath { string path; } Timer timer; void main(string[] args) { auto window = new MainWindow; auto ate = new AnimatedTextEdit(window); ate.content = ""; WidgetPainter.visualTheme = new MyTheme; auto configDir = getConfigPath("pdce", ""); if (!exists(configDir) || !isDir(configDir)) { mkdirRecurse(configDir); mkdirRecurse(configDir ~ "/syntax"); } auto configFile = buildPath(configDir, "config.ini"); auto syntaxFilesDir = configDir ~ "/syntax/"; string config = ensureFile(configFile, defaultConfig); string dsyntax = ensureFile(syntaxFilesDir ~ "d.ini", defaultDSyntax); if (args.length == 2 && isValidPath(args[1])) { openFilePath = args[1]; ate.content = readText(openFilePath); } refreshHighlight(syntaxFilesDir); struct Commands { @menu("File") { @accelerator("Ctrl+N") @hotkey('n') void New() { openFilePath = null; refreshHighlight(syntaxFilesDir); ate.content = ""; } @accelerator("Ctrl+O") @hotkey('s') void Open(FileName!() filename) { openFilePath = filename; refreshHighlight(syntaxFilesDir); ate.content = std.file.readText(filename); } } } Commands commands; window.setMenuAndToolbarFromAnnotatedCode(commands); timer = new Timer(16, { ate.redraw(); }); window.loop(); }