Files
starruler-linux/scripts/toolkit/elements/GuiTextbox.as
T
2018-07-17 14:15:37 +02:00

1051 lines
22 KiB
ActionScript

import elements.BaseGuiElement;
import elements.WordWrap;
export GuiTextbox;
class GuiTextbox : BaseGuiElement {
string Text;
string emptyText;
WordWrap@ wordWrap;
FontType TextFont = FT_Normal;
vec2i textOffset;
vec2i textSize;
bool Focused = false;
bool Hovered = false;
bool Dragging = false;
bool MultiLine = false;
bool EnterPressed = false;
bool disabled = false;
Color bgColor = colors::White;
Color textColor = skin.getColor(SC_Text);
Color selectionColor = skin.getColor(SC_Selected);
set_int characterLimit;
set_int characterExclude;
SkinStyle style = SS_Textbox;
int curs = 0;
int selpos = -1;
int scroll = 0;
int HorizPadding = 7;
int VertPadding = 3;
int lineHeight = 1;
int numLines = 0;
int textWidth = 0;
int fakeScrollX = -1;
double cursAct = 0.0;
GuiTextbox(IGuiElement@ ParentElement, const recti& Rectangle) {
super(ParentElement, Rectangle);
updateAbsolutePosition();
}
GuiTextbox(IGuiElement@ ParentElement, const recti& Rectangle, const string &in DefaultText) {
super(ParentElement, Rectangle);
text = DefaultText;
curs = DefaultText.length();
updateAbsolutePosition();
}
GuiTextbox(IGuiElement@ ParentElement, Alignment@ Align) {
super(ParentElement, Align);
updateAbsolutePosition();
}
GuiTextbox(IGuiElement@ ParentElement, Alignment@ Align, const string& DefaultText) {
super(ParentElement, Align);
text = DefaultText;
curs = DefaultText.length();
updateAbsolutePosition();
}
bool onGuiEvent(const GuiEvent& event) {
if(event.caller is this) {
switch(event.type) {
case GUI_Mouse_Entered:
Hovered = true;
break;
case GUI_Mouse_Left:
Hovered = false;
break;
case GUI_Focused:
Focused = true;
cursAct = frameTime;
break;
case GUI_Focus_Lost:
Focused = false;
Dragging = false;
selpos = -1;
break;
}
}
return BaseGuiElement::onGuiEvent(event);
}
void setIdentifierLimit() {
characterLimit.clear();
for(int i = 'a'; i <= 'z'; ++i)
characterLimit.insert(i);
for(int i = 'A'; i <= 'Z'; ++i)
characterLimit.insert(i);
for(int i = '0'; i <= '9'; ++i)
characterLimit.insert(i);
characterLimit.insert('_');
}
void setFilenameLimit() {
characterExclude.insert('/');
characterExclude.insert('\\');
characterExclude.insert('"');
characterExclude.insert('>');
characterExclude.insert('<');
characterExclude.insert(':');
characterExclude.insert('?');
characterExclude.insert('|');
characterExclude.insert('*');
}
void emitChange() {
GuiEvent evt;
evt.type = GUI_Changed;
@evt.caller = this;
onGuiEvent(evt);
}
void focus(bool selectAll = false) {
setGuiFocus(this);
curs = Text.length;
if(selectAll)
selpos = 0;
else
selpos = -1;
}
void clear() {
text = "";
}
bool onMouseEvent(const MouseEvent& event, IGuiElement@ source) {
if(source is this) {
switch(event.type) {
case MET_Button_Down: {
vec2i offset = vec2i(event.x, event.y) - AbsolutePosition.topLeft;
int pos = getOffsetChar(offset);
if(shiftKey && curs != pos) {
if(selpos == -1) {
selpos = curs;
curs = pos;
}
else {
curs = pos;
}
}
else {
curs = pos;
selpos = -1;
}
Dragging = true;
} return true;
case MET_Moved: {
if (Dragging) {
vec2i offset = vec2i(event.x, event.y) - AbsolutePosition.topLeft;
int pos = getOffsetChar(offset);
if(pos != curs) {
if(selpos == -1) {
selpos = curs;
curs = pos;
}
else {
curs = pos;
}
}
updateTextPosition();
cursAct = frameTime;
return true;
}
} break;
case MET_Button_Up:
if(Focused) {
Dragging = false;
return true;
}
break;
}
}
return BaseGuiElement::onMouseEvent(event, source);
}
void type(int code) {
if(characterLimit.size() != 0 && !characterLimit.contains(code)) {
sound::error.play();
return;
}
if(characterExclude.size() != 0 && characterExclude.contains(code)) {
sound::error.play();
return;
}
cursAct = frameTime;
int len = Text.length();
int tmp;
if(selpos >= 0) {
string newText;
int sell = -1, selr = -1;
if(selpos > curs) {
sell = curs;
selr = selpos;
}
else {
sell = selpos;
selr = curs;
}
if(sell > 0)
newText += Text.substr(0, sell);
u8append(newText, code);
if(len - selr > 0)
newText += Text.substr(selr, len - selr);
text = newText;
selpos = -1;
curs = sell;
updateTextPosition();
cursAct = frameTime;
u8next(Text, curs, tmp);
if(curs < 0)
curs = Text.length();
}
else {
if(curs == len) {
string newText = Text;
u8append(newText, code);
text = newText;
}
else if(curs == 0) {
string newText;
u8append(newText, code);
newText += Text;
text = newText;
}
else {
string newText;
newText += Text.substr(0, curs);
u8append(newText, code);
newText += Text.substr(curs, len - curs);
text = newText;
}
u8next(Text, curs, tmp);
len = Text.length();
if(curs < 0 || curs > len)
curs = len;
updateTextPosition();
}
}
bool onKeyEvent(const KeyboardEvent& event, IGuiElement@ source) {
if(Focused && !disabled) {
int tmp;
int len = Text.length();
switch(event.type) {
case KET_Key_Typed:
if(ctrlKey)
return false;
fakeScrollX = -1;
type(event.key);
emitChange();
return true;
case KET_Key_Down:
switch(event.key) {
case KEY_LOGICAL_A:
if(ctrlKey) {
curs = Text.length;
selpos = 0;
return true;
}
break;
case KEY_LOGICAL_C:
if(ctrlKey) {
if(selpos >= curs)
setClipboard(Text.substr(curs, selpos - curs));
else
setClipboard(Text.substr(selpos, curs - selpos));
return true;
}
break;
case KEY_LOGICAL_X:
if(ctrlKey) {
if(selpos >= 0 && curs >= 0) {
if(selpos >= curs)
setClipboard(Text.substr(curs, selpos - curs + 1));
else
setClipboard(Text.substr(selpos, curs - selpos));
//Remove current selection
if(selpos < curs) {
tmp = selpos;
selpos = curs;
curs = tmp;
}
text = Text.substr(0, curs) + Text.substr(selpos, len - selpos);
selpos = -1;
updateTextPosition();
cursAct = frameTime;
emitChange();
}
return true;
}
break;
case KEY_LOGICAL_V:
if(ctrlKey) {
//Remove current selection
if(selpos != -1) {
if(selpos < curs) {
tmp = selpos;
selpos = curs;
curs = tmp;
}
text = Text.substr(0, curs) + Text.substr(selpos, len - selpos);
selpos = -1;
}
//Insert clipboard text and advance cursor
string clipText = getClipboard();
text = Text.substr(0, curs) + clipText + Text.substr(curs, len - curs);
curs += clipText.length();
updateTextPosition();
emitChange();
return true;
}
break;
case KEY_BACKSPACE: {
if(curs <= 0 && selpos == -1)
break;
if(selpos == -1) {
int prevPos = curs;
u8prev(Text, prevPos, tmp);
string newText;
if(prevPos > 0)
newText += Text.substr(0, prevPos);
if(len - curs > 0)
newText += Text.substr(curs, len - curs);
text = newText;
curs = prevPos;
updateTextPosition();
cursAct = frameTime;
emitChange();
return true;
}
}
case KEY_DEL: {
fakeScrollX = -1;
if(selpos >= 0) {
string newText;
int sell = -1, selr = -1;
if(selpos > curs) {
sell = curs;
selr = selpos;
}
else {
sell = selpos;
selr = curs;
}
if(sell > 0)
newText += Text.substr(0, sell);
if(len - selr > 0)
newText += Text.substr(selr, len - selr);
text = newText;
selpos = -1;
curs = sell;
updateTextPosition();
cursAct = frameTime;
emitChange();
return true;
}
else if(curs < len) {
int prevPos = curs;
u8next(Text, prevPos, tmp);
string newText;
if(curs > 0)
newText += Text.substr(0, curs);
if(len - prevPos > 0)
newText += Text.substr(prevPos, len - prevPos);
text = newText;
updateTextPosition();
cursAct = frameTime;
emitChange();
return true;
}
} break;
case KEY_ESC:
if(selpos != -1) {
return true;
}
return BaseGuiElement::onKeyEvent(event, source);
case KEY_LEFT: {
cursAct = frameTime;
fakeScrollX = -1;
if(curs > 0) {
if(shiftKey) {
if(selpos == -1)
selpos = curs;
}
else {
selpos = -1;
}
u8prev(Text, curs, tmp);
if(curs < 0 || curs > len)
curs = 0;
updateTextPosition();
}
} return true;
case KEY_DOWN: {
if(curs >= 0 && MultiLine) {
if(shiftKey) {
if(selpos == -1)
selpos = curs;
}
else {
selpos = -1;
}
vec2i offset = getCharOffset(curs);
int line = getCharLine(curs);
if(line == scroll + numLines - 1) {
if(line == wordWrap.lineCount - 1)
break;
scroll += 1;
}
else {
offset.y += lineHeight;
}
if(fakeScrollX != -1)
offset.x = fakeScrollX;
else
fakeScrollX = offset.x;
curs = getOffsetChar(offset);
cursAct = frameTime;
updateTextPosition();
return true;
}
} return BaseGuiElement::onKeyEvent(event, source);
case KEY_UP: {
if(curs > 0 && MultiLine) {
if(shiftKey) {
if(selpos == -1)
selpos = curs;
}
else {
selpos = -1;
}
vec2i offset = getCharOffset(curs);
int line = getCharLine(curs);
if(line == scroll) {
if(line == 0)
break;
scroll -= 1;
}
else {
offset.y -= lineHeight;
}
if(fakeScrollX != -1)
offset.x = fakeScrollX;
else
fakeScrollX = offset.x;
curs = getOffsetChar(offset);
cursAct = frameTime;
updateTextPosition();
return true;
}
} return BaseGuiElement::onKeyEvent(event, source);
case KEY_PAGEDOWN: {
if(curs >= 0 && MultiLine) {
if(shiftKey) {
if(selpos == -1)
selpos = curs;
}
else {
selpos = -1;
}
vec2i offset = getCharOffset(curs);
scroll = min(wordWrap.lineCount - numLines, scroll + numLines);
if(fakeScrollX != -1)
offset.x = fakeScrollX;
else
fakeScrollX = offset.x;
curs = getOffsetChar(offset);
cursAct = frameTime;
updateTextPosition();
}
} return true;
case KEY_PAGEUP: {
if(curs > 0 && MultiLine) {
if(shiftKey) {
if(selpos == -1)
selpos = curs;
}
else {
selpos = -1;
}
vec2i offset = getCharOffset(curs);
scroll = max(0, scroll - numLines);
if(fakeScrollX != -1)
offset.x = fakeScrollX;
else
fakeScrollX = offset.x;
curs = getOffsetChar(offset);
cursAct = frameTime;
updateTextPosition();
}
} return true;
case KEY_HOME: {
if(shiftKey) {
if(selpos == -1)
selpos = curs;
}
else {
selpos = -1;
}
fakeScrollX = -1;
cursAct = frameTime;
if(MultiLine) {
int line = getCharLine(curs);
curs = wordWrap.positions[line];
}
else {
curs = 0;
}
updateTextPosition();
} return true;
case KEY_END: {
if(shiftKey) {
if(selpos == -1)
selpos = curs;
}
else {
selpos = -1;
}
fakeScrollX = -1;
cursAct = frameTime;
if(MultiLine) {
int line = getCharLine(curs);
curs = wordWrap.ends[line];
}
else {
curs = Text.length();
}
updateTextPosition();
} return true;
case KEY_RIGHT: {
cursAct = frameTime;
fakeScrollX = -1;
if(curs < len) {
if(shiftKey) {
if(selpos == -1)
selpos = curs;
}
else {
selpos = -1;
}
u8next(Text, curs, tmp);
if(curs < 0 || curs > len)
curs = len;
updateTextPosition();
}
} return true;
case KEY_ENTER: {
fakeScrollX = -1;
if(MultiLine) {
type('\n');
emitChanged();
}
else {
EnterPressed = true;
}
} return true;
case KEY_TAB:
if(!MultiLine)
return BaseGuiElement::onKeyEvent(event, source);
}
return !ctrlKey;
case KET_Key_Up:
switch(event.key) {
case KEY_F1: {
print("char:");
for(int i = 0; i < len; ++i) {
string debug = ""+Text[i]+": ";
u8append(debug, Text[i]);
print(debug);
}
print("utf8:");
int pos = 0, ch = 0;
while(pos >= 0) {
u8next(Text, pos, ch);
if(ch != 0) {
string debug = ""+ch+": ";
u8append(debug, ch);
print(debug);
}
}
} break;
case KEY_F2:
if(MultiLine) {
print("cursor: "+curs);
print("lines:");
for(uint i = 0, cnt = wordWrap.lineCount; i < cnt; ++i) {
print("Line "+i+": '"+wordWrap.lines[i]+"'");
print(""+wordWrap.positions[i]+" to "+wordWrap.ends[i]);
}
}
break;
case KEY_DOWN:
case KEY_UP:
if(!MultiLine)
return BaseGuiElement::onKeyEvent(event, source);
break;
case KEY_ESC:
if(selpos != -1) {
selpos = -1;
return true;
}
return BaseGuiElement::onKeyEvent(event, source);
case KEY_ENTER: {
if(!MultiLine && EnterPressed) {
GuiEvent evt;
evt.type = GUI_Confirmed;
@evt.caller = this;
onGuiEvent(evt);
EnterPressed = false;
}
} return true;
case KEY_TAB:
if(!MultiLine)
return BaseGuiElement::onKeyEvent(event, source);
}
return !ctrlKey;
}
}
return BaseGuiElement::onKeyEvent(event, source);
}
void centerText() {
const Font@ font = skin.getFont(TextFont);
if(!Focused && Text.length == 0)
textSize = font.getDimension(emptyText);
else
textSize = font.getDimension(Text);
textOffset.x = HorizPadding;
lineHeight = font.getLineHeight();
textWidth = AbsolutePosition.width - HorizPadding * 2;
if(MultiLine) {
wordWrap.width = textWidth;
textOffset.y = VertPadding;
}
else {
textOffset.y = (Position.height - font.getBaseline()) / 2;
}
}
void updateTextPosition() {
const Font@ font = skin.getFont(TextFont);
centerText();
if(!MultiLine) {
numLines = 1;
if(curs < scroll) {
scroll = curs;
}
else {
vec2i charOffset = getCharOffset(curs);
if(charOffset.x >= AbsolutePosition.width - padding)
scroll = getOffsetChar(vec2i(charOffset.x - textWidth + padding, 0));
}
}
else {
scroll = max(0, min(wordWrap.lineCount - 1, scroll));
numLines = max(floor(double(AbsolutePosition.height - 2 * VertPadding) / double(lineHeight)), 0.f);
while(curs < wordWrap.positions[scroll])
scroll -= 1;
while(scroll + numLines <= wordWrap.lineCount - 1 && curs >= wordWrap.positions[scroll + numLines])
scroll += 1;
}
}
int getOffsetChar(vec2i offset) {
const Font@ font = skin.getFont(TextFont);
int pos, line;
int c = 0, lastC = 0;
offset -= textOffset;
if(MultiLine) {
line = floor(double(offset.y - VertPadding) / double(lineHeight)) + scroll;
line = max(0, line);
line = min(wordWrap.lineCount - 1, line);
pos = wordWrap.positions[line];
}
else {
pos = scroll;
}
do {
if(MultiLine) {
if(line < wordWrap.lineCount - 1 && pos >= wordWrap.positions[line + 1]) {
int pos = wordWrap.positions[line + 1];
int tmp;
u8prev(Text, pos, tmp);
return pos;
}
}
int prevPos = pos;
u8next(Text, pos, c);
int w = font.getDimension(c, lastC).x;
lastC = c;
if(MultiLine && pos == -1)
return wordWrap.ends[line];
if(offset.x < w / 2)
return prevPos;
offset.x -= w;
}
while (pos >= 0 && pos < int(Text.length()));
return Text.length();
}
int getCharLine(int pos) {
if(!MultiLine)
return 0;
int line = scroll;
while(line < wordWrap.lineCount - 1 && pos >= wordWrap.positions[line + 1])
line += 1;
return line;
}
vec2i getCharOffset(int pos) {
const Font@ font = skin.getFont(TextFont);
if(MultiLine) {
if(pos < wordWrap.positions[scroll])
return vec2i();
//Find the line it's on
int line = getCharLine(pos);
//Find the position of the character
int x = padding;
int start = wordWrap.positions[line];
int c = 0, lastC = 0;
while(start < int(Text.length()) && start < pos) {
u8next(Text, start, c);
x += font.getDimension(c, lastC).x;
if(start == -1)
break;
lastC = c;
}
return vec2i(x, padding + (line - scroll) * lineHeight);
}
else {
if(pos < scroll)
return vec2i();
//Find the position of the character
int x = padding;
int start = scroll;
int c = 0, lastC = 0;
while(start < int(Text.length()) && start < pos) {
u8next(Text, start, c);
x += font.getDimension(c, lastC).x;
if(start == -1)
break;
lastC = c;
}
return vec2i(x, padding);
}
}
void set_font(FontType type) {
if(TextFont == type)
return;
TextFont = type;
centerText();
if(MultiLine) {
@wordWrap.font = skin.getFont(type);
wordWrap.update();
}
}
void set_text(const string& text) {
Text = text;
centerText();
if(MultiLine) {
wordWrap.text = text;
wordWrap.update();
}
if(text.length == 0)
scroll = 0;
else
scroll = clamp(scroll, 0, text.length-1);
}
string get_text() {
return Text;
}
void set_multiLine(bool multi) {
MultiLine = multi;
if(MultiLine) {
scroll = 0;
@wordWrap = WordWrap();
@wordWrap.font = skin.getFont(TextFont);
wordWrap.text = Text;
wordWrap.width = textWidth;
wordWrap.update();
}
else {
@wordWrap = null;
}
updateTextPosition();
}
void set_padding(int pad) {
HorizPadding = pad;
VertPadding = pad;
updateTextPosition();
if(MultiLine)
wordWrap.update();
}
int get_padding() {
return HorizPadding;
}
void updateAbsolutePosition() {
BaseGuiElement::updateAbsolutePosition();
if(Text.length != 0 || emptyText.length != 0) {
centerText();
if(MultiLine)
wordWrap.update();
}
}
void draw() {
uint flags = SF_Normal;
if(Focused)
flags |= SF_Focused;
if(Hovered)
flags |= SF_Hovered;
if(disabled)
flags |= SF_Disabled;
skin.draw(style, flags, AbsolutePosition, bgColor);
int sell = -1, selr = -1;
if(selpos >= 0) {
if(selpos > curs) {
sell = curs;
selr = selpos;
}
else {
sell = selpos;
selr = curs;
}
}
int pos = 0, ch = 0, prevCh = 0;
int bgOffset = (Position.height - textSize.y) / 2;
const Font@ font = skin.getFont(TextFont);
Color color = textColor;
if(Text.length == 0)
color.a = 0x80;
Color selColor = selectionColor;
vec2i drawPos = AbsolutePosition.topLeft + textOffset;
recti cursPos;
bool setCursor = false;
if(MultiLine) {
clipParent(recti(AbsolutePosition.topLeft + vec2i(HorizPadding, VertPadding),
AbsolutePosition.botRight - vec2i(HorizPadding, VertPadding)));
}
else {
clipParent(recti(AbsolutePosition.topLeft + vec2i(HorizPadding, 0),
AbsolutePosition.botRight - vec2i(HorizPadding, 0)));
}
double time = frameTime;
bool drawCurs = Focused && (time - floor(time) < 0.5 || time - cursAct < 0.5);
int line;
if(MultiLine) {
if(wordWrap.positions.length > uint(scroll))
pos = wordWrap.positions[scroll];
else
pos = 0;
line = scroll;
}
int startPos = pos;
while(pos >= 0) {
int prevPos = pos;
if(MultiLine) {
if(line < wordWrap.lineCount - 1 && pos >= wordWrap.positions[line + 1]) {
//Emit fake newlines if crossing line boundaries for wordwrap
line += 1;
prevPos -= 1;
ch = '\n';
}
else {
//Suppress actual newlines
if(!Focused && Text.length == 0)
u8next(emptyText, pos, ch);
else
u8next(Text, pos, ch);
if(ch == '\n' && pos != -1)
continue;
}
}
else {
if(!Focused && Text.length == 0)
u8next(emptyText, pos, ch);
else
u8next(Text, pos, ch);
}
if(ch != 0 && (MultiLine || prevPos >= scroll)) {
if(ch == 10 && MultiLine) {
if(!setCursor && drawCurs && curs == prevPos) {
cursPos = recti_area(drawPos, vec2i(1, lineHeight));
setCursor = true;
}
drawPos.y += lineHeight;
drawPos.x = AbsolutePosition.topLeft.x + textOffset.x;
prevCh = ch;
if(drawPos.y > AbsolutePosition.botRight.y)
break;
else
continue;
}
int width = font.getDimension(ch, prevCh).x;
if(drawPos.x + width > AbsolutePosition.botRight.x) {
if(MultiLine)
continue;
else
break;
}
if(prevPos >= sell && prevPos < selr) {
recti selPos = recti_area(drawPos, vec2i(width, lineHeight));
drawRectangle(selPos, selColor);
}
if(!setCursor && drawCurs && curs == prevPos) {
cursPos = recti_area(drawPos, vec2i(1, lineHeight));
setCursor = true;
}
skin.draw(TextFont, drawPos, ch, prevCh, color);
drawPos.x += width;
prevCh = ch;
}
}
if(drawCurs) {
clipParent();
if(!setCursor)
cursPos = recti_area(drawPos, vec2i(1, lineHeight));
drawRectangle(cursPos, color);
}
BaseGuiElement::draw();
}
};