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
/*
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
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
may be later, and should use native controls) to keep size down. Its only
dependencies are [arsd.simpledisplay] and [arsd.color].
may be later, and should use native controls) to keep size down. The Linux
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.
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
you can just drop into a small project and expect the basics to work without outside
dependencies, hopefully minigui will work for you.
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
@ -770,7 +782,7 @@ class Widget {
void focus() {
assert(parentWindow !is null);
if(parentWindow.focusedWidget is this)
if(isFocused())
return;
if(parentWindow.focusedWidget) {
@ -1015,6 +1027,8 @@ class Window : Widget {
dispatchKeyEvent(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);
},
);
@ -2049,6 +2063,20 @@ class Checkbox : MouseActivatedWidget {
super(parent);
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.fillColor = Color.white;
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);
};
defaultEventHandlers["click"] = delegate (Widget _this, Event ev) {
defaultEventHandlers["triggered"] = delegate (Widget _this, Event ev) {
isChecked = !isChecked;
auto event = new Event("change", this);
@ -2105,13 +2133,15 @@ class Radiobox : MouseActivatedWidget {
this.paint = (ScreenPainter painter) {
if(isFocused) {
painter.fillColor = windowBackgroundColor;
painter.outlineColor = Color.black;
painter.pen = Pen(Color.black, 1, Pen.Style.Dashed);
} else {
painter.fillColor = windowBackgroundColor;
painter.outlineColor = windowBackgroundColor;
}
painter.drawRectangle(Point(0, 0), width, height);
painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
painter.outlineColor = Color.black;
painter.fillColor = Color.white;
painter.drawEllipse(Point(2, 2), Point(height - 2, height - 2));
@ -2201,8 +2231,9 @@ class Button : MouseActivatedWidget {
if(isFocused()) {
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.pen = Pen(Color.black, 1, Pen.Style.Solid);
}
};
@ -2312,13 +2343,13 @@ abstract class EditableTextWidget : Widget {
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) {
if(parentWindow.win.closed) return;
this.focus();
textLayout.moveCaratToPixelCoordinates(ev.clientX, ev.clientY);
this.focus();
};
defaultEventHandlers["focus"] = delegate (Widget _this, Event ev) {
@ -2336,7 +2367,7 @@ abstract class EditableTextWidget : Widget {
caratTimer.destroy();
return;
}
if(parentWindow.focusedWidget is this) {
if(isFocused()) {
auto painter = this.draw();
textLayout.drawCarat(painter);
} else if(textLayout.caratShowingOnScreen) {
@ -2362,6 +2393,10 @@ abstract class EditableTextWidget : Widget {
};
addEventListener("keydown", delegate (Widget _this, Event ev) {
switch(ev.key) {
case Key.Delete:
textLayout.delete_();
redraw();
break;
case Key.Left:
textLayout.moveLeft(textLayout.carat);
redraw();
@ -2370,6 +2405,14 @@ abstract class EditableTextWidget : Widget {
textLayout.moveRight(textLayout.carat);
redraw();
break;
case Key.Up:
textLayout.moveUp(textLayout.carat);
redraw();
break;
case Key.Down:
textLayout.moveDown(textLayout.carat);
redraw();
break;
case Key.Home:
textLayout.moveHome(textLayout.carat);
redraw();

View File

@ -5149,7 +5149,10 @@ version(Windows) {
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);
if(x2 == 0 && y2 == 0)
TextOutW(hdc, x, y, buffer.ptr, cast(int) buffer.length);
@ -6148,7 +6151,7 @@ version(X11) {
Size textSize(string text) {
auto maxWidth = 0;
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')) {
int textWidth;
if(font)
@ -10432,6 +10435,18 @@ mixin template ExperimentalTextComponent() {
Rectangle boundingBox;
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) {
if(index < letterXs.length)
return letterXs[index];
@ -10473,6 +10488,36 @@ mixin template ExperimentalTextComponent() {
}
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,
@ -10502,6 +10547,15 @@ mixin template ExperimentalTextComponent() {
struct TextIdentifyResult {
InlineElement element;
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 {
@ -10575,14 +10629,11 @@ mixin template ExperimentalTextComponent() {
size_t lastLineIndex;
foreach(cidx, char a; arg) {
if(a == '\n') {
ie.text = arg[lastLineIndex .. cidx];
ie.text = arg[lastLineIndex .. cidx + 1];
lastLineIndex = cidx + 1;
ie.containingBlock = blocks[$-1];
blocks[$-1].parts ~= ie.clone;
ie.text = "\n";
ie.containingBlock = blocks[$-1];
blocks[$-1].parts ~= ie.clone;
addBlock();
ie.text = null;
} 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
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(part; block.parts) {
if(x >= part.boundingBox.left && x < part.boundingBox.right && y >= part.boundingBox.top && y < part.boundingBox.bottom) {
// FIXME binary search
size_t tidx;
foreach_reverse(idx, lx; part.letterXs)
int lastX;
foreach_reverse(idx, lx; part.letterXs) {
if(lx <= x) {
tidx = idx;
if(lastX && lastX - x < x - lx)
tidx = idx + 1;
else
tidx = idx;
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) {
@ -10632,14 +10719,17 @@ mixin template ExperimentalTextComponent() {
auto pos = Point(boundingBox.left, boundingBox.top);
int lastHeight;
foreach(block; blocks) {
void nl() {
pos.x = boundingBox.left;
pos.y += lastHeight;
foreach(ref part; block.parts) {
}
foreach(block; blocks) {
nl();
foreach(part; block.parts) {
painter.outlineColor = part.color;
painter.fillColor = part.backgroundColor;
if(part.text == "\n")
continue;
part.letterXs = null;
auto size = painter.textSize(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.letterXs = null;
foreach(idx, char c; part.text) {
// FIXME: unicode
part.letterXs ~= painter.textSize(part.text[0 .. idx]).width + pos.x;
@ -10664,14 +10753,20 @@ mixin template ExperimentalTextComponent() {
} else {
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) {
highlightSelection(painter);
drawCarat(painter);
} else {
eraseCarat(painter);
}
}
@ -10688,7 +10783,7 @@ mixin template ExperimentalTextComponent() {
y1 = boundingBox.top + 2;
y2 = boundingBox.top + painter.fontHeight;
} else {
x = carat.inlineElement.xOfIndex(carat.offset + 1);
x = carat.inlineElement.xOfIndex(carat.offset);
y1 = carat.inlineElement.boundingBox.top + 2;
y2 = carat.inlineElement.boundingBox.bottom - 2;
}
@ -10726,22 +10821,80 @@ mixin template ExperimentalTextComponent() {
/// 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) {}
void moveDown(ref Carat carat) {}
void moveUp(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) {
if(carat.inlineElement is null) return;
if(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) {
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++;
} else {
auto p = carat.inlineElement.getNextInlineElement();
if(p) {
carat.inlineElement = p;
carat.offset = 0;
}
}
}
void moveHome(ref Carat carat) {
carat.offset = 0;
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) {
if(carat.inlineElement)
carat.offset = carat.inlineElement.text.length;
auto x = int.max;
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 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.
void insert(string text) {}
void insert(dchar ch) {
if(ch == 127) {
delete_();
return;
}
if(ch == 8) {
backspace();
return;
}
if(ch == 13) ch = 10;
auto e = carat.inlineElement;
if(e is null) {
addText("" ~ cast(char) ch) ; // FIXME
@ -10765,27 +10923,55 @@ mixin template ExperimentalTextComponent() {
if(ch == 10) {
auto c = carat.inlineElement.clone;
c.text = null;
auto b = addBlock(c);
c.containingBlock = b;
b.parts ~= c;
insertPartAfter(c,e);
carat = Carat(this, c, 0);
}
} else {
// FIXME cast char sucks
if(ch == 10) {
auto c = carat.inlineElement.clone;
c.text = e.text[carat.offset + 1 .. $];
e.text = e.text[0 .. carat.offset + 1] ~ cast(char) ch;
auto b = addBlock(c);
c.containingBlock = b;
b.parts ~= c;
c.text = e.text[carat.offset .. $];
e.text = e.text[0 .. carat.offset] ~ cast(char) ch;
insertPartAfter(c,e);
carat = Carat(this, c, 0);
} 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++;
}
}
}
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() {
try_again:
auto e = carat.inlineElement;
@ -10793,22 +10979,29 @@ mixin template ExperimentalTextComponent() {
return;
if(carat.offset == 0) {
auto prev = e.getPreviousInlineElement();
auto newOffset = prev.text.length;
tryMerge(prev, e);
carat.inlineElement = prev;
carat.offset = prev is null ? 0 : prev.text.length;
carat.offset = prev is null ? 0 : newOffset;
goto try_again;
}
// FIXME: what if it spans parts?
if(carat.offset == e.text.length) {
} else if(carat.offset == e.text.length) {
e.text = e.text[0 .. $-1];
carat.offset--;
} 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--;
}
//cleanupStructures();
}
void delete_() {
auto after = carat;
moveRight(after);
if(carat != after) {
carat = after;
backspace();
}
}
void delete_() {}
void overstrike() {}
/// Selection API. See also: carat movement.