working minigui textedit on Linux

This commit is contained in:
Adam D. Ruppe 2017-04-01 23:29:49 -04:00
parent a909ac717c
commit 239ffedefd
2 changed files with 288 additions and 52 deletions

View File

@ -1,19 +1,31 @@
// http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx
/*
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
*/
/++ /++
minigui is a smallish GUI widget library, aiming to be on par with at least minigui is a smallish GUI widget library, aiming to be on par with at least
HTML4 forms and a few other expected gui components. It uses native controls HTML4 forms and a few other expected gui components. It uses native controls
on Windows and does its own thing on Linux (Mac is not currently supported but on Windows and does its own thing on Linux (Mac is not currently supported but
may be later, and should use native controls) to keep size down. Its only may be later, and should use native controls) to keep size down. The Linux
dependencies are [arsd.simpledisplay] and [arsd.color]. appearance is similar to Windows 95 and avoids using images to maintain network
efficiency on remote X connections.
minigui's only required dependencies are [arsd.simpledisplay] and [arsd.color].
Its #1 goal is to be useful without being large and complicated like GTK and Qt. Its #1 goal is to be useful without being large and complicated like GTK and Qt.
It isn't hugely concerned with appearance - on Windows, it just uses the native
controls and native theme, and on Linux, it keeps it simple and I may change that
at any time.
I love Qt, if you want something full featured, use it! But if you want something I love Qt, if you want something full featured, use it! But if you want something
you can just drop into a small project and expect the basics to work without outside you can just drop into a small project and expect the basics to work without outside
dependencies, hopefully minigui will work for you. dependencies, hopefully minigui will work for you.
The event model is similar to what you use in the browser with Javascript and the The event model is similar to what you use in the browser with Javascript and the
layout engine tries to automatically fit things in. layout engine tries to automatically fit things in, similar to a css flexbox.
FOR BEST RESULTS: be sure to link with the appropriate subsystem command FOR BEST RESULTS: be sure to link with the appropriate subsystem command
@ -770,7 +782,7 @@ class Widget {
void focus() { void focus() {
assert(parentWindow !is null); assert(parentWindow !is null);
if(parentWindow.focusedWidget is this) if(isFocused())
return; return;
if(parentWindow.focusedWidget) { if(parentWindow.focusedWidget) {
@ -1015,6 +1027,8 @@ class Window : Widget {
dispatchKeyEvent(e); dispatchKeyEvent(e);
}, },
(dchar e) { (dchar e) {
if(e == 13) e = 10; // hack?
if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
dispatchCharEvent(e); dispatchCharEvent(e);
}, },
); );
@ -2049,6 +2063,20 @@ class Checkbox : MouseActivatedWidget {
super(parent); super(parent);
this.paint = (ScreenPainter painter) { this.paint = (ScreenPainter painter) {
if(isFocused()) {
painter.pen = Pen(Color.black, 1, Pen.Style.Dashed);
painter.fillColor = windowBackgroundColor;
painter.drawRectangle(Point(0, 0), width, height);
painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
} else {
painter.pen = Pen(windowBackgroundColor, 1, Pen.Style.Solid);
painter.fillColor = windowBackgroundColor;
painter.drawRectangle(Point(0, 0), width, height);
}
painter.outlineColor = Color.black; painter.outlineColor = Color.black;
painter.fillColor = Color.white; painter.fillColor = Color.white;
painter.drawRectangle(Point(2, 2), height - 2, height - 2); painter.drawRectangle(Point(2, 2), height - 2, height - 2);
@ -2065,7 +2093,7 @@ class Checkbox : MouseActivatedWidget {
painter.drawText(Point(height + 4, 0), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); painter.drawText(Point(height + 4, 0), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
}; };
defaultEventHandlers["click"] = delegate (Widget _this, Event ev) { defaultEventHandlers["triggered"] = delegate (Widget _this, Event ev) {
isChecked = !isChecked; isChecked = !isChecked;
auto event = new Event("change", this); auto event = new Event("change", this);
@ -2105,13 +2133,15 @@ class Radiobox : MouseActivatedWidget {
this.paint = (ScreenPainter painter) { this.paint = (ScreenPainter painter) {
if(isFocused) { if(isFocused) {
painter.fillColor = windowBackgroundColor; painter.fillColor = windowBackgroundColor;
painter.outlineColor = Color.black; painter.pen = Pen(Color.black, 1, Pen.Style.Dashed);
} else { } else {
painter.fillColor = windowBackgroundColor; painter.fillColor = windowBackgroundColor;
painter.outlineColor = windowBackgroundColor; painter.outlineColor = windowBackgroundColor;
} }
painter.drawRectangle(Point(0, 0), width, height); painter.drawRectangle(Point(0, 0), width, height);
painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
painter.outlineColor = Color.black; painter.outlineColor = Color.black;
painter.fillColor = Color.white; painter.fillColor = Color.white;
painter.drawEllipse(Point(2, 2), Point(height - 2, height - 2)); painter.drawEllipse(Point(2, 2), Point(height - 2, height - 2));
@ -2201,8 +2231,9 @@ class Button : MouseActivatedWidget {
if(isFocused()) { if(isFocused()) {
painter.fillColor = Color.transparent; painter.fillColor = Color.transparent;
painter.outlineColor = Color.black; painter.pen = Pen(Color.black, 1, Pen.Style.Dashed);
painter.drawRectangle(Point(2, 2), width - 4, height - 4); painter.drawRectangle(Point(2, 2), width - 4, height - 4);
painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
} }
}; };
@ -2312,13 +2343,13 @@ abstract class EditableTextWidget : Widget {
textLayout.caratShowingOnScreen = false; textLayout.caratShowingOnScreen = false;
textLayout.drawInto(painter, !parentWindow.win.closed && parentWindow.focusedWidget is this); textLayout.drawInto(painter, !parentWindow.win.closed && isFocused());
}; };
defaultEventHandlers["click"] = delegate (Widget _this, Event ev) { defaultEventHandlers["click"] = delegate (Widget _this, Event ev) {
if(parentWindow.win.closed) return; if(parentWindow.win.closed) return;
this.focus();
textLayout.moveCaratToPixelCoordinates(ev.clientX, ev.clientY); textLayout.moveCaratToPixelCoordinates(ev.clientX, ev.clientY);
this.focus();
}; };
defaultEventHandlers["focus"] = delegate (Widget _this, Event ev) { defaultEventHandlers["focus"] = delegate (Widget _this, Event ev) {
@ -2336,7 +2367,7 @@ abstract class EditableTextWidget : Widget {
caratTimer.destroy(); caratTimer.destroy();
return; return;
} }
if(parentWindow.focusedWidget is this) { if(isFocused()) {
auto painter = this.draw(); auto painter = this.draw();
textLayout.drawCarat(painter); textLayout.drawCarat(painter);
} else if(textLayout.caratShowingOnScreen) { } else if(textLayout.caratShowingOnScreen) {
@ -2362,6 +2393,10 @@ abstract class EditableTextWidget : Widget {
}; };
addEventListener("keydown", delegate (Widget _this, Event ev) { addEventListener("keydown", delegate (Widget _this, Event ev) {
switch(ev.key) { switch(ev.key) {
case Key.Delete:
textLayout.delete_();
redraw();
break;
case Key.Left: case Key.Left:
textLayout.moveLeft(textLayout.carat); textLayout.moveLeft(textLayout.carat);
redraw(); redraw();
@ -2370,6 +2405,14 @@ abstract class EditableTextWidget : Widget {
textLayout.moveRight(textLayout.carat); textLayout.moveRight(textLayout.carat);
redraw(); redraw();
break; break;
case Key.Up:
textLayout.moveUp(textLayout.carat);
redraw();
break;
case Key.Down:
textLayout.moveDown(textLayout.carat);
redraw();
break;
case Key.Home: case Key.Home:
textLayout.moveHome(textLayout.carat); textLayout.moveHome(textLayout.carat);
redraw(); redraw();

View File

@ -5149,7 +5149,10 @@ version(Windows) {
return Size(rect.right, rect.bottom); return Size(rect.right, rect.bottom);
} }
void drawText(int x, int y, int x2, int y2, in char[] text, uint alignment) { void drawText(int x, int y, int x2, int y2, scope const(char)[] text, uint alignment) {
if(text.length && text[$-1] == '\n')
text = text[0 .. $-1]; // tailing newlines are weird on windows...
WCharzBuffer buffer = WCharzBuffer(text); WCharzBuffer buffer = WCharzBuffer(text);
if(x2 == 0 && y2 == 0) if(x2 == 0 && y2 == 0)
TextOutW(hdc, x, y, buffer.ptr, cast(int) buffer.length); TextOutW(hdc, x, y, buffer.ptr, cast(int) buffer.length);
@ -6148,7 +6151,7 @@ version(X11) {
Size textSize(string text) { Size textSize(string text) {
auto maxWidth = 0; auto maxWidth = 0;
auto lineHeight = fontHeight; auto lineHeight = fontHeight;
int h = 0; int h = text.length ? 0 : lineHeight + 4; // if text is empty, it still gives the line height
foreach(line; text.split('\n')) { foreach(line; text.split('\n')) {
int textWidth; int textWidth;
if(font) if(font)
@ -10432,6 +10435,18 @@ mixin template ExperimentalTextComponent() {
Rectangle boundingBox; Rectangle boundingBox;
int[] letterXs; // FIXME: maybe i should do bounding boxes for every character int[] letterXs; // FIXME: maybe i should do bounding boxes for every character
bool isMergeCompatible(InlineElement other) {
return
containingBlock is other.containingBlock &&
color == other.color &&
backgroundColor == other.backgroundColor &&
styles == other.styles &&
font == other.font &&
fontSize == other.fontSize &&
lineHeight == other.lineHeight &&
true;
}
int xOfIndex(size_t index) { int xOfIndex(size_t index) {
if(index < letterXs.length) if(index < letterXs.length)
return letterXs[index]; return letterXs[index];
@ -10473,6 +10488,36 @@ mixin template ExperimentalTextComponent() {
} }
return prev; return prev;
} }
InlineElement getNextInlineElement() {
InlineElement next = null;
foreach(idx, ie; this.containingBlock.parts) {
if(ie is this) {
if(idx + 1 < this.containingBlock.parts.length)
next = this.containingBlock.parts[idx + 1];
break;
}
}
if(next is null) {
BlockElement n;
foreach(idx, ie; this.containingBlock.containingLayout.blocks) {
if(ie is this.containingBlock) {
if(idx + 1 < this.containingBlock.containingLayout.blocks.length)
n = this.containingBlock.containingLayout.blocks[idx + 1];
break;
}
}
if(n is null)
return null;
if(n.parts.length)
next = n.parts[0];
else {} // FIXME
}
return next;
}
} }
// Block elements are used entirely for positioning inline elements, // Block elements are used entirely for positioning inline elements,
@ -10502,6 +10547,15 @@ mixin template ExperimentalTextComponent() {
struct TextIdentifyResult { struct TextIdentifyResult {
InlineElement element; InlineElement element;
size_t offset; size_t offset;
private TextIdentifyResult fixupNewline() {
if(element !is null && offset < element.text.length && element.text[offset] == '\n') {
offset--;
} else if(element !is null && offset == element.text.length && element.text.length > 1 && element.text[$-1] == '\n') {
offset--;
}
return this;
}
} }
class TextLayout { class TextLayout {
@ -10575,14 +10629,11 @@ mixin template ExperimentalTextComponent() {
size_t lastLineIndex; size_t lastLineIndex;
foreach(cidx, char a; arg) { foreach(cidx, char a; arg) {
if(a == '\n') { if(a == '\n') {
ie.text = arg[lastLineIndex .. cidx]; ie.text = arg[lastLineIndex .. cidx + 1];
lastLineIndex = cidx + 1; lastLineIndex = cidx + 1;
ie.containingBlock = blocks[$-1]; ie.containingBlock = blocks[$-1];
blocks[$-1].parts ~= ie.clone; blocks[$-1].parts ~= ie.clone;
ie.text = "\n"; ie.text = null;
ie.containingBlock = blocks[$-1];
blocks[$-1].parts ~= ie.clone;
addBlock();
} else { } else {
} }
@ -10596,30 +10647,66 @@ mixin template ExperimentalTextComponent() {
} }
} }
void tryMerge(InlineElement into, InlineElement what) {
if(!into.isMergeCompatible(what)) {
return; // cannot merge, different configs
}
// cool, can merge, bring text together...
into.text ~= what.text;
// and remove what
for(size_t a = 0; a < what.containingBlock.parts.length; a++) {
if(what.containingBlock.parts[a] is what) {
for(size_t i = a; i < what.containingBlock.parts.length - 1; i++)
what.containingBlock.parts[i] = what.containingBlock.parts[i + 1];
what.containingBlock.parts = what.containingBlock.parts[0 .. $-1];
}
}
// FIXME: ensure no other carats have a reference to it
}
/// Call this if the inputs change. It will reflow everything /// Call this if the inputs change. It will reflow everything
void redoLayout() { void redoLayout() {
} }
TextIdentifyResult identify(int x, int y) { /// exact = true means return null if no match. otherwise, get the closest one that makes sense for a mouse click.
TextIdentifyResult identify(int x, int y, bool exact = false) {
TextIdentifyResult inexactMatch;
foreach(block; blocks) { foreach(block; blocks) {
foreach(part; block.parts) { foreach(part; block.parts) {
if(x >= part.boundingBox.left && x < part.boundingBox.right && y >= part.boundingBox.top && y < part.boundingBox.bottom) { if(x >= part.boundingBox.left && x < part.boundingBox.right && y >= part.boundingBox.top && y < part.boundingBox.bottom) {
// FIXME binary search // FIXME binary search
size_t tidx; size_t tidx;
foreach_reverse(idx, lx; part.letterXs) int lastX;
foreach_reverse(idx, lx; part.letterXs) {
if(lx <= x) { if(lx <= x) {
if(lastX && lastX - x < x - lx)
tidx = idx + 1;
else
tidx = idx; tidx = idx;
break; break;
} }
lastX = lx;
}
return TextIdentifyResult(part, tidx); return TextIdentifyResult(part, tidx).fixupNewline;
} else if(!exact) {
// we're not in the box, but are we on the same line?
if(y >= part.boundingBox.top && y < part.boundingBox.bottom)
inexactMatch = TextIdentifyResult(part, x == 0 ? 0 : part.text.length);
} }
} }
} }
return TextIdentifyResult(null, 0); if(!exact && inexactMatch is TextIdentifyResult.init && blocks.length && blocks[$-1].parts.length)
return TextIdentifyResult(blocks[$-1].parts[$-1], blocks[$-1].parts[$-1].text.length).fixupNewline;
return exact ? TextIdentifyResult.init : inexactMatch.fixupNewline;
} }
void moveCaratToPixelCoordinates(int x, int y) { void moveCaratToPixelCoordinates(int x, int y) {
@ -10632,14 +10719,17 @@ mixin template ExperimentalTextComponent() {
auto pos = Point(boundingBox.left, boundingBox.top); auto pos = Point(boundingBox.left, boundingBox.top);
int lastHeight; int lastHeight;
foreach(block; blocks) { void nl() {
pos.x = boundingBox.left; pos.x = boundingBox.left;
pos.y += lastHeight; pos.y += lastHeight;
foreach(ref part; block.parts) { }
foreach(block; blocks) {
nl();
foreach(part; block.parts) {
painter.outlineColor = part.color; painter.outlineColor = part.color;
painter.fillColor = part.backgroundColor; painter.fillColor = part.backgroundColor;
if(part.text == "\n") part.letterXs = null;
continue;
auto size = painter.textSize(part.text); auto size = painter.textSize(part.text);
painter.drawText(pos, part.text); painter.drawText(pos, part.text);
@ -10650,7 +10740,6 @@ mixin template ExperimentalTextComponent() {
part.boundingBox = Rectangle(pos.x, pos.y, pos.x + size.width, pos.y + size.height); part.boundingBox = Rectangle(pos.x, pos.y, pos.x + size.width, pos.y + size.height);
part.letterXs = null;
foreach(idx, char c; part.text) { foreach(idx, char c; part.text) {
// FIXME: unicode // FIXME: unicode
part.letterXs ~= painter.textSize(part.text[0 .. idx]).width + pos.x; part.letterXs ~= painter.textSize(part.text[0 .. idx]).width + pos.x;
@ -10664,14 +10753,20 @@ mixin template ExperimentalTextComponent() {
} else { } else {
lastHeight = size.height; lastHeight = size.height;
} }
if(part.text.length && part.text[$-1] == '\n')
nl();
} }
} }
// on every redraw, I will force the carat to be
// redrawn too, in order to eliminate perceived lag
// when moving around with the mouse.
eraseCarat(painter);
if(focused) { if(focused) {
highlightSelection(painter); highlightSelection(painter);
drawCarat(painter); drawCarat(painter);
} else {
eraseCarat(painter);
} }
} }
@ -10688,7 +10783,7 @@ mixin template ExperimentalTextComponent() {
y1 = boundingBox.top + 2; y1 = boundingBox.top + 2;
y2 = boundingBox.top + painter.fontHeight; y2 = boundingBox.top + painter.fontHeight;
} else { } else {
x = carat.inlineElement.xOfIndex(carat.offset + 1); x = carat.inlineElement.xOfIndex(carat.offset);
y1 = carat.inlineElement.boundingBox.top + 2; y1 = carat.inlineElement.boundingBox.top + 2;
y2 = carat.inlineElement.boundingBox.bottom - 2; y2 = carat.inlineElement.boundingBox.bottom - 2;
} }
@ -10726,22 +10821,80 @@ mixin template ExperimentalTextComponent() {
/// Carat movement api /// Carat movement api
/// These should give the user a logical result based on what they see on screen... /// 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!) /// thus they locate predominately by *pixels* not char index. (These will generally coincide with monospace fonts tho!)
void moveUp(ref Carat carat) {} void moveUp(ref Carat carat) {
void moveDown(ref Carat carat) {} auto x = carat.inlineElement.xOfIndex(carat.offset);
auto y = carat.inlineElement.boundingBox.top + 2;
y -= carat.inlineElement.boundingBox.bottom - carat.inlineElement.boundingBox.top;
auto i = identify(x, y);
if(i.element) {
carat.inlineElement = i.element;
carat.offset = i.offset;
}
}
void moveDown(ref Carat carat) {
auto x = carat.inlineElement.xOfIndex(carat.offset);
auto y = carat.inlineElement.boundingBox.bottom - 2;
y += carat.inlineElement.boundingBox.bottom - carat.inlineElement.boundingBox.top;
auto i = identify(x, y);
if(i.element) {
carat.inlineElement = i.element;
carat.offset = i.offset;
}
}
void moveLeft(ref Carat carat) { void moveLeft(ref Carat carat) {
if(carat.inlineElement is null) return;
if(carat.offset) if(carat.offset)
carat.offset--; carat.offset--;
else {
auto p = carat.inlineElement.getPreviousInlineElement();
if(p) {
carat.inlineElement = p;
if(p.text.length && p.text[$-1] == '\n')
carat.offset = p.text.length - 1;
else
carat.offset = p.text.length;
}
}
} }
void moveRight(ref Carat carat) { void moveRight(ref Carat carat) {
if(carat.inlineElement && carat.offset < carat.inlineElement.text.length) if(carat.inlineElement is null) return;
if(carat.offset < carat.inlineElement.text.length && carat.inlineElement.text[carat.offset] != '\n') {
carat.offset++; carat.offset++;
} } else {
void moveHome(ref Carat carat) { auto p = carat.inlineElement.getNextInlineElement();
if(p) {
carat.inlineElement = p;
carat.offset = 0; carat.offset = 0;
} }
}
}
void moveHome(ref Carat carat) {
auto x = 0;
auto y = carat.inlineElement.boundingBox.top + 2;
auto i = identify(x, y);
if(i.element) {
carat.inlineElement = i.element;
carat.offset = i.offset;
}
}
void moveEnd(ref Carat carat) { void moveEnd(ref Carat carat) {
if(carat.inlineElement) auto x = int.max;
carat.offset = carat.inlineElement.text.length; auto y = carat.inlineElement.boundingBox.top + 2;
auto i = identify(x, y);
if(i.element) {
carat.inlineElement = i.element;
carat.offset = i.offset;
}
} }
void movePageUp(ref Carat carat) {} void movePageUp(ref Carat carat) {}
void movePageDown(ref Carat carat) {} void movePageDown(ref Carat carat) {}
@ -10749,10 +10902,15 @@ mixin template ExperimentalTextComponent() {
/// Plain text editing api. These work at the current carat inside the selected inline element. /// Plain text editing api. These work at the current carat inside the selected inline element.
void insert(string text) {} void insert(string text) {}
void insert(dchar ch) { void insert(dchar ch) {
if(ch == 127) {
delete_();
return;
}
if(ch == 8) { if(ch == 8) {
backspace(); backspace();
return; return;
} }
if(ch == 13) ch = 10;
auto e = carat.inlineElement; auto e = carat.inlineElement;
if(e is null) { if(e is null) {
addText("" ~ cast(char) ch) ; // FIXME addText("" ~ cast(char) ch) ; // FIXME
@ -10765,27 +10923,55 @@ mixin template ExperimentalTextComponent() {
if(ch == 10) { if(ch == 10) {
auto c = carat.inlineElement.clone; auto c = carat.inlineElement.clone;
c.text = null; c.text = null;
auto b = addBlock(c); insertPartAfter(c,e);
c.containingBlock = b;
b.parts ~= c;
carat = Carat(this, c, 0); carat = Carat(this, c, 0);
} }
} else { } else {
// FIXME cast char sucks // FIXME cast char sucks
if(ch == 10) { if(ch == 10) {
auto c = carat.inlineElement.clone; auto c = carat.inlineElement.clone;
c.text = e.text[carat.offset + 1 .. $]; c.text = e.text[carat.offset .. $];
e.text = e.text[0 .. carat.offset + 1] ~ cast(char) ch; e.text = e.text[0 .. carat.offset] ~ cast(char) ch;
auto b = addBlock(c); insertPartAfter(c,e);
c.containingBlock = b;
b.parts ~= c;
carat = Carat(this, c, 0); carat = Carat(this, c, 0);
} else { } else {
e.text = e.text[0 .. carat.offset + 1] ~ cast(char) ch ~ e.text[carat.offset + 1 .. $]; e.text = e.text[0 .. carat.offset] ~ cast(char) ch ~ e.text[carat.offset .. $];
carat.offset++; carat.offset++;
} }
} }
} }
void insertPartAfter(InlineElement what, InlineElement where) {
foreach(idx, p; where.containingBlock.parts) {
if(p is where) {
if(idx + 1 == where.containingBlock.parts.length)
where.containingBlock.parts ~= what;
else
where.containingBlock.parts = where.containingBlock.parts[0 .. idx + 1] ~ what ~ where.containingBlock.parts[idx + 1 .. $];
return;
}
}
}
void cleanupStructures() {
for(size_t i = 0; i < blocks.length; i++) {
auto block = blocks[i];
for(size_t a = 0; a < block.parts.length; a++) {
auto part = block.parts[a];
if(part.text.length == 0) {
for(size_t b = a; b < block.parts.length - 1; b++)
block.parts[b] = block.parts[b+1];
block.parts = block.parts[0 .. $-1];
}
}
if(block.parts.length == 0) {
for(size_t a = i; a < blocks.length - 1; a++)
blocks[a] = blocks[a+1];
blocks = blocks[0 .. $-1];
}
}
}
void backspace() { void backspace() {
try_again: try_again:
auto e = carat.inlineElement; auto e = carat.inlineElement;
@ -10793,22 +10979,29 @@ mixin template ExperimentalTextComponent() {
return; return;
if(carat.offset == 0) { if(carat.offset == 0) {
auto prev = e.getPreviousInlineElement(); auto prev = e.getPreviousInlineElement();
auto newOffset = prev.text.length;
tryMerge(prev, e);
carat.inlineElement = prev; carat.inlineElement = prev;
carat.offset = prev is null ? 0 : prev.text.length; carat.offset = prev is null ? 0 : newOffset;
goto try_again; goto try_again;
} } else if(carat.offset == e.text.length) {
// FIXME: what if it spans parts?
if(carat.offset == e.text.length) {
e.text = e.text[0 .. $-1]; e.text = e.text[0 .. $-1];
carat.offset--; carat.offset--;
} else { } else {
e.text = e.text[0 .. carat.offset] ~ e.text[carat.offset + 1 .. $]; e.text = e.text[0 .. carat.offset - 1] ~ e.text[carat.offset .. $];
carat.offset--; carat.offset--;
} }
//cleanupStructures();
}
void delete_() {
auto after = carat;
moveRight(after);
if(carat != after) {
carat = after;
backspace();
}
} }
void delete_() {}
void overstrike() {} void overstrike() {}
/// Selection API. See also: carat movement. /// Selection API. See also: carat movement.