From df9c912bd9197362085832bb3719eb73afbfbb28 Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Mon, 3 Apr 2017 21:08:07 -0400 Subject: [PATCH] font api in sdpy --- color.d | 4 + minigui.d | 270 +++++++++++++++++++++++++++++++++++++++++------- simpledisplay.d | 198 ++++++++++++++++++++++++++++++++++- 3 files changed, 428 insertions(+), 44 deletions(-) diff --git a/color.d b/color.d index 06f4e9a..ef3554b 100644 --- a/color.d +++ b/color.d @@ -1256,6 +1256,10 @@ struct Point { Point opBinary(string op)(Point rhs) { return Point(mixin("x" ~ op ~ "rhs.x"), mixin("y" ~ op ~ "rhs.y")); } + + Point opBinary(string op)(int rhs) { + return Point(mixin("x" ~ op ~ "rhs"), mixin("y" ~ op ~ "rhs")); + } } /// diff --git a/minigui.d b/minigui.d index df81964..e02482f 100644 --- a/minigui.d +++ b/minigui.d @@ -1,5 +1,18 @@ // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx +/* + TODO: + + scrolling + event cleanup + ScreenPainter dtor stuff. clipping api. + Windows radio button sizing and theme text selection + tooltips. + api improvements + + margins are kinda broken, they don't collapse like they should. at least. +*/ + /* 1(15:19:48) NotSpooky: Menus, text entry, label, notebook, box, frame, file dialogs and layout (this one is very useful because I can draw lines between its child widgets @@ -123,6 +136,27 @@ abstract class ComboboxBase : Widget { else version(custom_widgets) this(Widget parent = null) { super(parent); + + addEventListener("keydown", (Event event) { + if(event.key == Key.Up) { + if(selection > -1) { // -1 means select blank + selection--; + auto t = new Event("change", this); + t.dispatch(); + } + event.preventDefault(); + } + if(event.key == Key.Down) { + if(selection + 1 < options.length) { + selection++; + auto t = new Event("change", this); + t.dispatch(); + } + event.preventDefault(); + } + + }); + } else static assert(false); @@ -139,12 +173,15 @@ abstract class ComboboxBase : Widget { selection = idx; version(win32_widgets) SendMessageA(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); + + auto t = new Event("change", this); + t.dispatch(); } version(win32_widgets) override void handleWmCommand(ushort cmd, ushort id) { selection = SendMessageA(hwnd, 327 /* CB_GETCURSEL */, 0, 0); - auto event = new Event("changed", this); + auto event = new Event("change", this); event.dispatch(); } @@ -177,12 +214,12 @@ abstract class ComboboxBase : Widget { dropDown.setEventHandlers( (MouseEvent event) { - if(event.type == MouseEventType.buttonPressed) { + if(event.type == MouseEventType.buttonReleased) { auto element = (event.y - 4) / Window.lineHeight; if(element >= 0 && element <= options.length) { selection = element; - auto t = new Event("changed", this); + auto t = new Event("change", this); t.dispatch(); } dropDown.close(); @@ -211,12 +248,36 @@ class DropDownSelection : ComboboxBase { draw3dFrame(this, painter, FrameStyle.risen); painter.outlineColor = Color.black; painter.drawText(Point(4, 4), selection == -1 ? "" : options[selection]); - painter.drawLine(Point(width - 4 - 8, 4), Point(width - 4 - 2, height - 2)); - painter.drawLine(Point(width - 2, 4), Point(width - 4 - 2, height - 2)); + + painter.outlineColor = Color.black; + painter.fillColor = Color.black; + Point[3] triangle; + enum padding = 6; + enum paddingV = 8; + enum triangleWidth = 10; + triangle[0] = Point(width - padding - triangleWidth, paddingV); + triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); + triangle[2] = Point(width - padding - 0, paddingV); + painter.drawPolygon(triangle[]); + + if(isFocused()) { + painter.fillColor = Color.transparent; + painter.pen = Pen(Color.black, 1, Pen.Style.Dashed); + painter.drawRectangle(Point(2, 2), width - 4, height - 4); + painter.pen = Pen(Color.black, 1, Pen.Style.Solid); + + } + }; - addEventListener("changed", &this.redraw); - addEventListener("click", &this.popup); + addEventListener("focus", &this.redraw); + addEventListener("blur", &this.redraw); + addEventListener("change", &this.redraw); + addEventListener("mousedown", () { this.focus(); this.popup(); }); + addEventListener("keydown", (Event event) { + if(event.key == Key.Space) + popup(); + }); } else static assert(false); } } @@ -233,7 +294,8 @@ class FreeEntrySelection : ComboboxBase { super(parent); auto hl = new HorizontalLayout(this); lineEdit = new LineEdit(hl); - lineEdit.content = selection == -1 ? "" : options[selection]; + + tabStop = false; auto btn = new class Button { this() { @@ -243,9 +305,11 @@ class FreeEntrySelection : ComboboxBase { return 16; } }; + //btn.addDirectEventListener("focus", &lineEdit.focus); btn.addEventListener("triggered", &this.popup); - addEventListener("changed", { - lineEdit.content = selection == -1 ? "" : options[selection]; + addEventListener("change", { + lineEdit.content = (selection == -1 ? "" : options[selection]); + lineEdit.focus(); redraw(); }); } @@ -278,6 +342,30 @@ class ComboBox : ComboboxBase { } lineEdit.content = c; }); + + listWidget.tabStop = false; + this.tabStop = false; + listWidget.addEventListener("focus", &lineEdit.focus); + this.addEventListener("focus", &lineEdit.focus); + + addDirectEventListener("change", { + listWidget.setSelection(selection); + if(selection != -1) + lineEdit.content = options[selection]; + lineEdit.focus(); + redraw(); + }); + + listWidget.addDirectEventListener("change", { + int set = -1; + foreach(idx, opt; listWidget.options) + if(opt.selected) { + set = cast(int) idx; + break; + } + if(set != selection) + this.setSelection(set); + }); } else static assert(false); } @@ -307,21 +395,28 @@ class ListWidget : Widget { bool selected; } + void setSelection(int y) { + if(!multiSelect) + foreach(ref opt; options) + opt.selected = false; + if(y >= 0 && y < options.length) + options[y].selected = !options[y].selected; + + auto evt = new Event("change", this); + evt.dispatch(); + + redraw(); + + } + this(Widget parent = null) { super(parent); defaultEventHandlers["click"] = delegate(Widget _this, Event event) { + this.focus(); auto y = (event.clientY - 4) / Window.lineHeight; if(y >= 0 && y < options.length) { - if(!multiSelect) - foreach(ref opt; options) - opt.selected = false; - options[y].selected = !options[y].selected; - - auto evt = new Event("change", this); - evt.dispatch(); - - redraw(); + setSelection(y); } }; @@ -588,6 +683,7 @@ mixin template LayoutInfo() { foreach(child; children) { sum += child.minHeight(); sum += child.marginTop(); + sum += child.marginBottom(); } return sum; @@ -806,6 +902,19 @@ version(win32_widgets) { assert(p.hwnd !is null); + + static HFONT font; + if(font is null) { + NONCLIENTMETRICS params; + params.cbSize = params.sizeof; + if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { + font = CreateFontIndirect(¶ms.lfMessageFont); + } + } + + if(font) + SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); + Widget.nativeMapping[p.hwnd] = p; p.originalWindowProcedure = cast(WNDPROC) SetWindowLong(p.hwnd, GWL_WNDPROC, cast(LONG) &HookedWndProc); @@ -993,7 +1102,7 @@ class Widget { parentWindow.focusedWidget = this; auto evt = new Event("focus", this); - evt.sendDirectly(); + evt.dispatch(); } @@ -1206,10 +1315,10 @@ class Window : Widget { win.onFocusChange = (bool getting) { if(this.focusedWidget) { auto evt = new Event(getting ? "focus" : "blur", this.focusedWidget); - evt.sendDirectly(); + evt.dispatch(); } auto evt = new Event(getting ? "focus" : "blur", this); - evt.sendDirectly(); + evt.dispatch(); }; win.setEventHandlers( @@ -1606,8 +1715,8 @@ class ToolBar : Widget { override int minHeight() { return idealHeight; } override int maxHeight() { return idealHeight; } } else version(custom_widgets) { - override int minHeight() { return Window.lineHeight * 3/2; } - override int maxHeight() { return Window.lineHeight * 3/2; } + override int minHeight() { return 32; }// Window.lineHeight * 3/2; } + override int maxHeight() { return 32; } //Window.lineHeight * 3/2; } } else static assert(false); override int heightStretchiness() { return 0; } @@ -1685,7 +1794,61 @@ class ToolButton : Button { this.draw3dFrame(painter, isDepressed ? FrameStyle.sunk : FrameStyle.risen, currentButtonColor); painter.outlineColor = Color.black; - painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); + + enum iconSize = 32; + enum multiplier = iconSize / 16; + switch(action.iconId) { + case GenericIcons.New: + painter.fillColor = Color.white; + painter.drawPolygon( + Point(3, 2) * multiplier, Point(3, 13) * multiplier, Point(12, 13) * multiplier, Point(12, 6) * multiplier, + Point(8, 2) * multiplier, Point(8, 6) * multiplier, Point(12, 6) * multiplier, Point(8, 2) * multiplier + ); + break; + case GenericIcons.Save: + painter.fillColor = Color.black; + painter.drawRectangle(Point(2, 2) * multiplier, Point(13, 13) * multiplier); + + painter.fillColor = Color.white; + painter.outlineColor = Color.white; + painter.drawRectangle(Point(6, 3) * multiplier, Point(9, 5) * multiplier); + painter.drawRectangle(Point(5, 9) * multiplier, Point(10, 12) * multiplier); + break; + case GenericIcons.Open: + painter.fillColor = Color.white; + painter.drawPolygon( + Point(2, 4) * multiplier, Point(2, 12) * multiplier, Point(13, 12) * multiplier, Point(13, 3) * multiplier, + Point(9, 3) * multiplier, Point(9, 4) * multiplier, Point(2, 4) * multiplier); + painter.drawLine(Point(3, 6) * multiplier, Point(9, 6) * multiplier); + painter.drawLine(Point(9, 7) * multiplier, Point(13, 7) * multiplier); + break; + case GenericIcons.Copy: + painter.fillColor = Color.white; + painter.drawRectangle(Point(3, 2) * multiplier, Point(9, 10) * multiplier); + painter.drawRectangle(Point(6, 5) * multiplier, Point(12, 13) * multiplier); + break; + case GenericIcons.Cut: + painter.fillColor = Color.transparent; + painter.drawLine(Point(3, 2) * multiplier, Point(10, 9) * multiplier); + painter.drawLine(Point(4, 9) * multiplier, Point(11, 2) * multiplier); + painter.drawRectangle(Point(3, 9) * multiplier, Point(5, 13) * multiplier); + painter.drawRectangle(Point(9, 9) * multiplier, Point(11, 12) * multiplier); + break; + case GenericIcons.Paste: + painter.fillColor = Color.white; + painter.drawRectangle(Point(2, 3) * multiplier, Point(11, 11) * multiplier); + painter.drawRectangle(Point(6, 8) * multiplier, Point(13, 13) * multiplier); + painter.drawLine(Point(6, 2) * multiplier, Point(4, 5) * multiplier); + painter.drawLine(Point(7, 2) * multiplier, Point(9, 5) * multiplier); + painter.fillColor = Color.black; + painter.drawRectangle(Point(4, 5) * multiplier, Point(9, 6) * multiplier); + break; + case GenericIcons.Help: + painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); + break; + default: + painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); + } }; } else static assert(false); @@ -1693,7 +1856,10 @@ class ToolButton : Button { Action action; - override int maxWidth() { return 40; } + override int maxWidth() { return 32; } + override int minWidth() { return 32; } + override int maxHeight() { return 32; } + override int minHeight() { return 32; } } @@ -1854,7 +2020,7 @@ class StatusBar : Widget { assert(idealHeight); } else version(custom_widgets) { this.paint = (ScreenPainter painter) { - this.draw3dFrame(painter, FrameStyle.risen); + this.draw3dFrame(painter, FrameStyle.sunk); int cpos = 0; int remainingLength = this.width; foreach(idx, part; this.partsArray) { @@ -2287,9 +2453,15 @@ else static assert(false); /// class Checkbox : MouseActivatedWidget { - override int maxHeight() { return 16; } - override int minHeight() { return 16; } - mixin Margin!"4"; + version(win32_widgets) { + override int maxHeight() { return 16; } + override int minHeight() { return 16; } + } else version(custom_widgets) { + override int maxHeight() { return Window.lineHeight; } + override int minHeight() { return Window.lineHeight; } + } else static assert(0); + + override int marginLeft() { return 4; } version(win32_widgets) this(string label, Widget parent = null) { @@ -2315,21 +2487,23 @@ class Checkbox : MouseActivatedWidget { } + enum buttonSize = 16; painter.outlineColor = Color.black; painter.fillColor = Color.white; - painter.drawRectangle(Point(2, 2), height - 2, height - 2); + painter.drawRectangle(Point(2, 2), buttonSize - 2, buttonSize - 2); if(isChecked) { painter.pen = Pen(Color.black, 2); // I'm using height so the checkbox is square - painter.drawLine(Point(6, 6), Point(height - 4, height - 4)); - painter.drawLine(Point(height-4, 6), Point(6, height - 4)); + enum padding = 5; + painter.drawLine(Point(padding, padding), Point(buttonSize - (padding-2), buttonSize - (padding-2))); + painter.drawLine(Point(buttonSize-(padding-2), padding), Point(padding, buttonSize - (padding-2))); painter.pen = Pen(Color.black, 1); } - painter.drawText(Point(height + 4, 0), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); + painter.drawText(Point(buttonSize + 4, 0), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); }; defaultEventHandlers["triggered"] = delegate (Widget _this, Event ev) { @@ -2355,8 +2529,16 @@ class VerticalSpacer : Widget { /// class Radiobox : MouseActivatedWidget { - override int maxHeight() { return 16; } - override int minHeight() { return 16; } + + version(win32_widgets) { + override int maxHeight() { return 16; } + override int minHeight() { return 16; } + } else version(custom_widgets) { + override int maxHeight() { return Window.lineHeight; } + override int minHeight() { return Window.lineHeight; } + } else static assert(0); + + override int marginLeft() { return 4; } version(win32_widgets) this(string label, Widget parent = null) { @@ -2382,18 +2564,19 @@ class Radiobox : MouseActivatedWidget { painter.pen = Pen(Color.black, 1, Pen.Style.Solid); + enum buttonSize = 16; + painter.outlineColor = Color.black; painter.fillColor = Color.white; - painter.drawEllipse(Point(2, 2), Point(height - 2, height - 2)); - + painter.drawEllipse(Point(2, 2), Point(buttonSize - 2, buttonSize - 2)); if(isChecked) { painter.outlineColor = Color.black; painter.fillColor = Color.black; // I'm using height so the checkbox is square - painter.drawEllipse(Point(5, 5), Point(height - 5, height - 5)); + painter.drawEllipse(Point(5, 5), Point(buttonSize - 5, buttonSize - 5)); } - painter.drawText(Point(height + 4, 0), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); + painter.drawText(Point(buttonSize + 4, 0), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); }; defaultEventHandlers["triggered"] = delegate (Widget _this, Event ev) { @@ -2771,6 +2954,13 @@ mixin template EventStuff() { EventHandler[][string] capturingEventHandlers; EventHandler[string] defaultEventHandlers; + void addDirectEventListener(string event, void delegate() handler, bool useCapture = false) { + addEventListener(event, (Widget, Event e) { + if(e.srcElement is this) + handler(); + }, useCapture); + } + void addEventListener(string event, void delegate() handler, bool useCapture = false) { addEventListener(event, (Widget, Event) { handler(); }, useCapture); } diff --git a/simpledisplay.d b/simpledisplay.d index 4e0fd95..685adf3 100644 --- a/simpledisplay.d +++ b/simpledisplay.d @@ -3990,6 +3990,122 @@ void displayImage(Image image, SimpleWindow win = null) { } } +enum FontWeight : int { + dontcare = 0, + thin = 100, + extralight = 200, + light = 300, + regular = 400, + medium = 500, + semibold = 600, + bold = 700, + extrabold = 800, + heavy = 900 +} + +/++ + Represents a font loaded off the operating system or the X server. + + + While the api here is unified cross platform, the fonts are not necessarily + available, even across machines of the same platform, so be sure to always check + for null (using [isNull]) and have a fallback plan. + + When you have a font you like, use [ScreenPainter.setFont] to load it for drawing. + + Worst case, a null font will automatically fall back to the default font loaded + for your system. ++/ +class OperatingSystemFont { + + version(X11) { + XFontStruct* font; + XFontSet fontset; + } else version(Windows) { + HFONT font; + } else static assert(0); + + /// + this(string name, int size = 0, FontWeight weight = FontWeight.dontcare, bool italic = false) { + load(name, size, weight, italic); + } + + /// + bool load(string name, int size = 0, FontWeight weight = FontWeight.dontcare, bool italic = false) { + unload(); + version(X11) { + string weightstr; + with(FontWeight) + final switch(weight) { + case dontcare: weightstr = "*"; break; + case thin: weightstr = "extralight"; break; + case extralight: weightstr = "extralight"; break; + case light: weightstr = "light"; break; + case regular: weightstr = "regular"; break; + case medium: weightstr = "medium"; break; + case semibold: weightstr = "demibold"; break; + case bold: weightstr = "bold"; break; + case extrabold: weightstr = "demibold"; break; + case heavy: weightstr = "black"; break; + } + string sizestr; + if(size == 0) + sizestr = "*"; + else + sizestr = "" ~ cast(char)(size / 10 + '0') ~ cast(char)(size % 10 + '0'); + auto xfontstr = "-*-"~name~"-"~weightstr~"-"~(italic ? "i" : "r")~"-*-*-"~sizestr~"-*-*-*-*-*-*-*"; + + //import std.stdio; writeln(xfontstr); + + auto display = XDisplayConnection.get; + + font = XLoadQueryFont(display, xfontstr.ptr); + if(font is null) + return false; + + char** lol; + int lol2; + char* lol3; + fontset = XCreateFontSet(display, xfontstr.ptr, &lol, &lol2, &lol3); + } else version(Windows) { + WCharzBuffer buffer = WCharzBuffer(name); + font = CreateFont(size, 0, 0, 0, cast(int) weight, italic, 0, 0, 0, 0, 0, 0, 0, buffer.ptr); + } else static assert(0); + + return !isNull(); + } + + /// + void unload() { + if(isNull()) + return; + + version(X11) { + auto display = XDisplayConnection.get; + + if(font) + XFreeFont(display, font); + if(fontset) + XFreeFontSet(display, fontset); + + font = null; + fontset = null; + } else version(Windows) { + DeleteObject(font); + font = null; + } else static assert(0); + } + + /// + bool isNull() { + return font is null; + } + + ~this() { + unload(); + } +} + /** The 2D drawing proxy. You acquire one of these with [SimpleWindow.draw] rather than constructing it directly. Then, it is reference counted so you can pass it @@ -4036,6 +4152,11 @@ struct ScreenPainter { //writeln("refcount ++ ", impl.referenceCount); } + /// + void setFont(OperatingSystemFont font) { + impl.setFont(font); + } + /// int fontHeight() { return impl.fontHeight(); @@ -4200,6 +4321,13 @@ struct ScreenPainter { impl.drawRectangle(upperLeft.x, upperLeft.y, width, height); } + void drawRectangle(Point upperLeft, Point lowerRightInclusive) { + transform(upperLeft); + transform(lowerRightInclusive); + impl.drawRectangle(upperLeft.x, upperLeft.y, + lowerRightInclusive.x - upperLeft.x + 1, lowerRightInclusive.y - upperLeft.y + 1); + } + /// Arguments are the points of the bounding rectangle void drawEllipse(Point upperLeft, Point lowerRight) { transform(upperLeft); @@ -4215,11 +4343,16 @@ struct ScreenPainter { /// . void drawPolygon(Point[] vertexes) { - foreach(vertex; vertexes) + foreach(ref vertex; vertexes) transform(vertex); impl.drawPolygon(vertexes); } + /// ditto + void drawPolygon(Point[] vertexes...) { + drawPolygon(vertexes); + } + // and do a draw/fill in a single call maybe. Windows can do it... but X can't, though it could do two calls. @@ -5030,6 +5163,31 @@ version(Windows) { // X doesn't draw a text background, so neither should we SetBkMode(hdc, TRANSPARENT); + + + static bool triedDefaultGuiFont = false; + if(!triedDefaultGuiFont) { + NONCLIENTMETRICS params; + params.cbSize = params.sizeof; + if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { + defaultGuiFont = CreateFontIndirect(¶ms.lfMessageFont); + } + triedDefaultGuiFont = true; + } + + if(defaultGuiFont) { + SelectObject(hdc, defaultGuiFont); + // DeleteObject(defaultGuiFont); + } + } + + static HFONT defaultGuiFont; + + void setFont(OperatingSystemFont font) { + if(font && font.font) + SelectObject(hdc, font.font); + else if(defaultGuiFont) + SelectObject(hdc, defaultGuiFont); } // just because we can on Windows... @@ -6009,9 +6167,13 @@ version(X11) { // FIXME: should the gc be static too so it isn't recreated every time draw is called? GC gc; - __gshared XFontStruct* font; __gshared bool fontAttempted; - __gshared XFontSet fontset; + + __gshared XFontStruct* defaultfont; + __gshared XFontSet defaultfontset; + + XFontStruct* font; + XFontSet fontset; void create(NativeWindowHandle window) { this.display = XDisplayConnection.get(); @@ -6039,13 +6201,31 @@ version(X11) { fontset = XCreateFontSet(display, xfontstr.ptr, &lol, &lol2, &lol3); fontAttempted = true; + + defaultfont = font; + defaultfontset = fontset; } + font = defaultfont; + fontset = defaultfontset; + if(font) { XSetFont(display, gc, font.fid); } } + void setFont(OperatingSystemFont font) { + if(font && font.font) { + this.font = font.font; + this.fontset = font.fontset; + XSetFont(display, gc, font.font.fid); + } else { + this.font = defaultfont; + this.fontset = defaultfontset; + } + + } + void dispose() { auto buffer = this.window.impl.buffer; @@ -6311,8 +6491,12 @@ version(X11) { } void drawPolygon(Point[] vertexes) { + XPoint[16] pointsBuffer; XPoint[] points; - points.length = vertexes.length; + if(vertexes.length <= pointsBuffer.length) + points = pointsBuffer[0 .. vertexes.length]; + else + points.length = vertexes.length; foreach(i, p; vertexes) { points[i].x = cast(short) p.x; @@ -10837,6 +11021,7 @@ mixin template ExperimentalTextComponent() { Point(x, y1), Point(x, y2) ); + painter.rasterOp = RasterOp.normal; caratShowingOnScreen = !caratShowingOnScreen; if(caratShowingOnScreen) { @@ -10856,12 +11041,14 @@ mixin template ExperimentalTextComponent() { ); caratShowingOnScreen = false; + painter.rasterOp = RasterOp.normal; } /// Carat movement api /// These should give the user a logical result based on what they see on screen... /// thus they locate predominately by *pixels* not char index. (These will generally coincide with monospace fonts tho!) void moveUp(ref Carat carat) { + if(carat.inlineElement is null) return; auto x = carat.inlineElement.xOfIndex(carat.offset); auto y = carat.inlineElement.boundingBox.top + 2; @@ -10875,6 +11062,7 @@ mixin template ExperimentalTextComponent() { } } void moveDown(ref Carat carat) { + if(carat.inlineElement is null) return; auto x = carat.inlineElement.xOfIndex(carat.offset); auto y = carat.inlineElement.boundingBox.bottom - 2; @@ -10914,6 +11102,7 @@ mixin template ExperimentalTextComponent() { } } void moveHome(ref Carat carat) { + if(carat.inlineElement is null) return; auto x = 0; auto y = carat.inlineElement.boundingBox.top + 2; @@ -10925,6 +11114,7 @@ mixin template ExperimentalTextComponent() { } } void moveEnd(ref Carat carat) { + if(carat.inlineElement is null) return; auto x = int.max; auto y = carat.inlineElement.boundingBox.top + 2;