2019-04-24 17:56:16 +00:00
|
|
|
// Copyright 2019 Google LLC.
|
|
|
|
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
// [Work In Progress] Proof of principle of a text editor written with Skia & SkShaper.
|
|
|
|
// https://bugs.skia.org/9020
|
|
|
|
|
2019-04-28 18:40:45 +00:00
|
|
|
#include "include/core/SkCanvas.h"
|
2019-04-24 17:56:16 +00:00
|
|
|
#include "include/core/SkSurface.h"
|
|
|
|
#include "include/core/SkTime.h"
|
2019-04-28 18:40:45 +00:00
|
|
|
|
2019-07-08 20:07:57 +00:00
|
|
|
#include "tools/ModifierKey.h"
|
2019-04-24 17:56:16 +00:00
|
|
|
#include "tools/sk_app/Application.h"
|
|
|
|
#include "tools/sk_app/Window.h"
|
|
|
|
|
2019-05-14 19:01:39 +00:00
|
|
|
#include "experimental/editor/editor.h"
|
2019-04-28 18:40:45 +00:00
|
|
|
|
2019-04-24 17:56:16 +00:00
|
|
|
#include <memory>
|
2019-06-20 15:29:10 +00:00
|
|
|
#include <fstream>
|
2019-04-28 18:40:45 +00:00
|
|
|
|
2019-06-20 15:29:10 +00:00
|
|
|
#ifdef SK_EDITOR_DEBUG_OUT
|
2019-04-28 18:40:45 +00:00
|
|
|
static const char* key_name(sk_app::Window::Key k) {
|
|
|
|
switch (k) {
|
|
|
|
#define M(X) case sk_app::Window::Key::k ## X: return #X
|
|
|
|
M(NONE); M(LeftSoftKey); M(RightSoftKey); M(Home); M(Back); M(Send); M(End); M(0); M(1);
|
|
|
|
M(2); M(3); M(4); M(5); M(6); M(7); M(8); M(9); M(Star); M(Hash); M(Up); M(Down); M(Left);
|
|
|
|
M(Right); M(Tab); M(PageUp); M(PageDown); M(Delete); M(Escape); M(Shift); M(Ctrl);
|
|
|
|
M(Option); M(A); M(C); M(V); M(X); M(Y); M(Z); M(OK); M(VolUp); M(VolDown); M(Power);
|
|
|
|
M(Camera);
|
|
|
|
#undef M
|
|
|
|
default: return "?";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-08 20:07:57 +00:00
|
|
|
static SkString modifiers_desc(ModifierKey m) {
|
2019-04-28 18:40:45 +00:00
|
|
|
SkString s;
|
2019-07-08 20:07:57 +00:00
|
|
|
#define M(X) if (m & ModifierKey::k ## X ##) { s.append(" {" #X "}"); }
|
2019-04-28 18:40:45 +00:00
|
|
|
M(Shift) M(Control) M(Option) M(Command) M(FirstPress)
|
|
|
|
#undef M
|
|
|
|
return s;
|
|
|
|
}
|
2019-04-24 17:56:16 +00:00
|
|
|
|
2019-07-08 20:07:57 +00:00
|
|
|
static void debug_on_char(SkUnichar c, ModifierKey modifiers) {
|
2019-05-03 21:14:21 +00:00
|
|
|
SkString m = modifiers_desc(modifiers);
|
|
|
|
if ((unsigned)c < 0x100) {
|
|
|
|
SkDebugf("char: %c (0x%02X)%s\n", (char)(c & 0xFF), (unsigned)c, m.c_str());
|
|
|
|
} else {
|
|
|
|
SkDebugf("char: 0x%08X%s\n", (unsigned)c, m.c_str());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-08 20:07:57 +00:00
|
|
|
static void debug_on_key(sk_app::Window::Key key, sk_app::Window::InputState, ModifierKey modi) {
|
|
|
|
SkDebugf("key: %s%s\n", key_name(key), modifiers_desc(modi).c_str());
|
2019-05-03 21:14:21 +00:00
|
|
|
}
|
2019-06-20 15:29:10 +00:00
|
|
|
#endif // SK_EDITOR_DEBUG_OUT
|
2019-05-03 21:14:21 +00:00
|
|
|
|
|
|
|
static editor::Editor::Movement convert(sk_app::Window::Key key) {
|
|
|
|
switch (key) {
|
|
|
|
case sk_app::Window::Key::kLeft: return editor::Editor::Movement::kLeft;
|
|
|
|
case sk_app::Window::Key::kRight: return editor::Editor::Movement::kRight;
|
|
|
|
case sk_app::Window::Key::kUp: return editor::Editor::Movement::kUp;
|
|
|
|
case sk_app::Window::Key::kDown: return editor::Editor::Movement::kDown;
|
|
|
|
case sk_app::Window::Key::kHome: return editor::Editor::Movement::kHome;
|
|
|
|
case sk_app::Window::Key::kEnd: return editor::Editor::Movement::kEnd;
|
|
|
|
default: return editor::Editor::Movement::kNowhere;
|
|
|
|
}
|
|
|
|
}
|
2019-04-24 17:56:16 +00:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
struct Timer {
|
|
|
|
double fTime;
|
|
|
|
const char* fDesc;
|
|
|
|
Timer(const char* desc = "") : fTime(SkTime::GetNSecs()), fDesc(desc) {}
|
2019-04-30 16:38:28 +00:00
|
|
|
~Timer() { SkDebugf("%s: %5d μs\n", fDesc, (int)((SkTime::GetNSecs() - fTime) * 1e-3)); }
|
2019-04-24 17:56:16 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
struct EditorLayer : public sk_app::Window::Layer {
|
2019-06-20 15:29:10 +00:00
|
|
|
SkString fPath;
|
2019-04-24 17:56:16 +00:00
|
|
|
sk_app::Window* fParent = nullptr;
|
2019-06-20 15:29:10 +00:00
|
|
|
editor::StringSlice fClipboard;
|
2019-04-28 18:40:45 +00:00
|
|
|
editor::Editor fEditor;
|
2019-05-03 21:14:21 +00:00
|
|
|
editor::Editor::TextPosition fTextPos{0, 0};
|
|
|
|
editor::Editor::TextPosition fMarkPos;
|
2019-04-28 18:40:45 +00:00
|
|
|
int fPos = 0; // window pixel position in file
|
|
|
|
int fWidth = 0; // window width
|
|
|
|
int fHeight = 0; // window height
|
2019-05-03 21:14:21 +00:00
|
|
|
bool fShiftDown = false;
|
|
|
|
|
|
|
|
EditorLayer() {
|
2019-06-20 15:29:10 +00:00
|
|
|
fEditor.setFont(SkFont(SkTypeface::MakeFromName("monospace", SkFontStyle()), 18));
|
2019-05-03 21:14:21 +00:00
|
|
|
}
|
2019-04-28 18:40:45 +00:00
|
|
|
|
|
|
|
void loadFile(const char* path) {
|
|
|
|
if (sk_sp<SkData> data = SkData::MakeFromFileName(path)) {
|
2019-06-20 15:29:10 +00:00
|
|
|
fPath = path;
|
2019-04-28 18:40:45 +00:00
|
|
|
fEditor.setText((const char*)data->data(), data->size());
|
2019-06-20 15:29:10 +00:00
|
|
|
} else {
|
|
|
|
fPath = "output.txt";
|
2019-04-28 18:40:45 +00:00
|
|
|
}
|
|
|
|
}
|
2019-04-24 17:56:16 +00:00
|
|
|
|
|
|
|
void onPaint(SkSurface* surface) override {
|
2019-05-03 21:14:21 +00:00
|
|
|
SkCanvas* canvas = surface->getCanvas();
|
|
|
|
SkAutoCanvasRestore acr(canvas, true);
|
|
|
|
canvas->clipRect({0, 0, (float)fWidth, (float)fHeight});
|
|
|
|
canvas->translate(0, -(float)fPos);
|
|
|
|
editor::Editor::PaintOpts options;
|
|
|
|
options.fCursor = fTextPos;
|
|
|
|
options.fBackgroundColor = SkColor4f{0.8f, 0.8f, 0.8f, 1};
|
|
|
|
if (fMarkPos != editor::Editor::TextPosition()) {
|
|
|
|
options.fSelectionBegin = fMarkPos;
|
|
|
|
options.fSelectionEnd = fTextPos;
|
|
|
|
}
|
2019-06-20 15:29:10 +00:00
|
|
|
{
|
|
|
|
#ifdef SK_EDITOR_DEBUG_OUT
|
|
|
|
Timer timer("shaping");
|
|
|
|
#endif // SK_EDITOR_DEBUG_OUT
|
|
|
|
fEditor.paint(nullptr, options);
|
|
|
|
}
|
|
|
|
#ifdef SK_EDITOR_DEBUG_OUT
|
|
|
|
Timer timer("painting");
|
|
|
|
#endif // SK_EDITOR_DEBUG_OUT
|
2019-05-03 21:14:21 +00:00
|
|
|
fEditor.paint(canvas, options);
|
2019-04-24 17:56:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void onResize(int width, int height) override {
|
|
|
|
if (SkISize{fWidth, fHeight} != SkISize{width, height}) {
|
|
|
|
fHeight = height;
|
|
|
|
if (width != fWidth) {
|
|
|
|
fWidth = width;
|
2019-04-28 18:40:45 +00:00
|
|
|
fEditor.setWidth(fWidth);
|
2019-04-24 17:56:16 +00:00
|
|
|
}
|
2019-05-03 21:14:21 +00:00
|
|
|
this->inval();
|
2019-04-24 17:56:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void onAttach(sk_app::Window* w) override { fParent = w; }
|
|
|
|
|
2019-05-03 21:14:21 +00:00
|
|
|
bool scroll(int delta) {
|
2019-04-28 18:40:45 +00:00
|
|
|
int maxPos = std::max(0, fEditor.getHeight() - fHeight / 2);
|
|
|
|
int newpos = std::max(0, std::min(fPos + delta, maxPos));
|
2019-04-24 17:56:16 +00:00
|
|
|
if (newpos != fPos) {
|
|
|
|
fPos = newpos;
|
2019-05-03 21:14:21 +00:00
|
|
|
this->inval();
|
2019-04-24 17:56:16 +00:00
|
|
|
}
|
2019-05-03 21:14:21 +00:00
|
|
|
return true;
|
2019-04-28 18:40:45 +00:00
|
|
|
}
|
|
|
|
|
2019-05-03 21:14:21 +00:00
|
|
|
void inval() { if (fParent) { fParent->inval(); } }
|
|
|
|
|
2019-07-08 20:07:57 +00:00
|
|
|
bool onMouseWheel(float delta, ModifierKey modifiers) override {
|
2019-04-28 18:40:45 +00:00
|
|
|
this->scroll(-(int)(delta * fEditor.font().getSpacing()));
|
|
|
|
return true;
|
2019-04-24 17:56:16 +00:00
|
|
|
}
|
|
|
|
|
2019-07-08 20:07:57 +00:00
|
|
|
bool onMouse(int x, int y, sk_app::Window::InputState state, ModifierKey modifiers) override {
|
2019-04-24 17:56:16 +00:00
|
|
|
if (sk_app::Window::kDown_InputState == state) {
|
2019-04-28 18:40:45 +00:00
|
|
|
y += fPos;
|
2019-05-03 21:14:21 +00:00
|
|
|
editor::Editor::TextPosition pos = fEditor.getPosition(SkIPoint{x, y});
|
2019-06-20 15:29:10 +00:00
|
|
|
#ifdef SK_EDITOR_DEBUG_OUT
|
2019-05-03 21:14:21 +00:00
|
|
|
SkDebugf("select: line:%d column:%d \n", pos.fParagraphIndex, pos.fTextByteIndex);
|
2019-06-20 15:29:10 +00:00
|
|
|
#endif // SK_EDITOR_DEBUG_OUT
|
2019-05-03 21:14:21 +00:00
|
|
|
if (pos != editor::Editor::TextPosition()) {
|
|
|
|
fTextPos = pos;
|
|
|
|
this->inval();
|
2019-04-24 17:56:16 +00:00
|
|
|
}
|
|
|
|
}
|
2019-05-03 21:14:21 +00:00
|
|
|
return true;
|
2019-04-24 17:56:16 +00:00
|
|
|
}
|
2019-04-28 18:40:45 +00:00
|
|
|
|
2019-07-08 20:07:57 +00:00
|
|
|
bool onChar(SkUnichar c, ModifierKey modifiers) override {
|
|
|
|
if (!ModifierKeyIsSet(modifiers & (ModifierKey::kControl |
|
|
|
|
ModifierKey::kOption |
|
|
|
|
ModifierKey::kCommand))) {
|
2019-06-20 15:29:10 +00:00
|
|
|
if (((unsigned)c < 0x7F && (unsigned)c >= 0x20) || c == '\n') {
|
2019-05-03 21:14:21 +00:00
|
|
|
char ch = (char)c;
|
|
|
|
fEditor.insert(fTextPos, &ch, 1);
|
2019-06-20 15:29:10 +00:00
|
|
|
#ifdef SK_EDITOR_DEBUG_OUT
|
2019-05-03 21:14:21 +00:00
|
|
|
SkDebugf("insert: %X'%c'\n", (unsigned)c, ch);
|
2019-06-20 15:29:10 +00:00
|
|
|
#endif // SK_EDITOR_DEBUG_OUT
|
2019-05-03 21:14:21 +00:00
|
|
|
return this->moveCursor(editor::Editor::Movement::kRight);
|
|
|
|
}
|
|
|
|
}
|
2019-07-08 20:07:57 +00:00
|
|
|
if (modifiers == ModifierKey::kControl) {
|
2019-06-20 15:29:10 +00:00
|
|
|
switch (c) {
|
|
|
|
case 'p':
|
|
|
|
for (const editor::StringSlice& str : fEditor.text()) {
|
|
|
|
SkDebugf(">> '%.*s'\n", str.size(), str.begin());
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
case 's':
|
|
|
|
{
|
|
|
|
std::ofstream out(fPath.c_str());
|
|
|
|
for (const editor::StringSlice& str : fEditor.text()) {
|
|
|
|
out.write(str.begin(), str.size());
|
|
|
|
out.write("\n", 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
case 'c':
|
|
|
|
if (fMarkPos != editor::Editor::TextPosition()) {
|
|
|
|
fClipboard = fEditor.copy(fMarkPos, fTextPos);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
case 'x':
|
|
|
|
if (fMarkPos != editor::Editor::TextPosition()) {
|
|
|
|
fClipboard = fEditor.copy(fMarkPos, fTextPos);
|
|
|
|
fTextPos = fEditor.remove(fMarkPos, fTextPos);
|
|
|
|
this->inval();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
case 'v':
|
|
|
|
if (fClipboard.size()) {
|
|
|
|
fEditor.insert(fTextPos, fClipboard.begin(), fClipboard.size());
|
|
|
|
this->inval();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
#ifdef SK_EDITOR_DEBUG_OUT
|
2019-05-03 21:14:21 +00:00
|
|
|
debug_on_char(c, modifiers);
|
2019-06-20 15:29:10 +00:00
|
|
|
#endif // SK_EDITOR_DEBUG_OUT
|
2019-05-03 21:14:21 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
bool moveCursor(editor::Editor::Movement m, bool shift = false) {
|
|
|
|
if (shift != fShiftDown) {
|
|
|
|
fMarkPos = shift ? fTextPos : editor::Editor::TextPosition();
|
|
|
|
fShiftDown = shift;
|
|
|
|
}
|
|
|
|
fTextPos = fEditor.move(m, fTextPos);
|
|
|
|
this->inval();
|
2019-04-28 18:40:45 +00:00
|
|
|
return true;
|
|
|
|
}
|
2019-05-03 21:14:21 +00:00
|
|
|
bool onKey(sk_app::Window::Key key,
|
|
|
|
sk_app::Window::InputState state,
|
2019-07-08 20:07:57 +00:00
|
|
|
ModifierKey modifiers) override {
|
2019-04-28 18:40:45 +00:00
|
|
|
if (state == sk_app::Window::kDown_InputState) {
|
2019-05-03 21:14:21 +00:00
|
|
|
switch (key) {
|
|
|
|
case sk_app::Window::Key::kPageDown:
|
|
|
|
return this->scroll(fHeight * 4 / 5);
|
|
|
|
case sk_app::Window::Key::kPageUp:
|
|
|
|
return this->scroll(-fHeight * 4 / 5);
|
|
|
|
case sk_app::Window::Key::kLeft:
|
|
|
|
case sk_app::Window::Key::kRight:
|
|
|
|
case sk_app::Window::Key::kUp:
|
|
|
|
case sk_app::Window::Key::kDown:
|
|
|
|
case sk_app::Window::Key::kHome:
|
|
|
|
case sk_app::Window::Key::kEnd:
|
|
|
|
return this->moveCursor(convert(key),
|
2019-07-08 20:07:57 +00:00
|
|
|
(int)(modifiers & ModifierKey::kShift));
|
2019-05-03 21:14:21 +00:00
|
|
|
case sk_app::Window::Key::kDelete:
|
|
|
|
if (fMarkPos != editor::Editor::TextPosition()) {
|
|
|
|
fTextPos = fEditor.remove(fMarkPos, fTextPos);
|
|
|
|
fMarkPos = editor::Editor::TextPosition();
|
|
|
|
} else {
|
|
|
|
fTextPos = fEditor.remove(fTextPos,
|
|
|
|
fEditor.move(editor::Editor::Movement::kRight,
|
|
|
|
fTextPos));
|
|
|
|
}
|
|
|
|
this->inval();
|
|
|
|
return true;
|
|
|
|
case sk_app::Window::Key::kBack:
|
|
|
|
if (fMarkPos != editor::Editor::TextPosition()) {
|
|
|
|
fTextPos = fEditor.remove(fMarkPos, fTextPos);
|
|
|
|
fMarkPos = editor::Editor::TextPosition();
|
|
|
|
} else {
|
|
|
|
fTextPos = fEditor.remove(fTextPos,
|
|
|
|
fEditor.move(editor::Editor::Movement::kLeft,
|
|
|
|
fTextPos));
|
|
|
|
}
|
|
|
|
this->inval();
|
|
|
|
return true;
|
2019-06-20 15:29:10 +00:00
|
|
|
case sk_app::Window::Key::kOK:
|
|
|
|
return this->onChar('\n', modifiers);
|
2019-05-03 21:14:21 +00:00
|
|
|
default:
|
|
|
|
break;
|
2019-04-28 18:40:45 +00:00
|
|
|
}
|
2019-06-20 15:29:10 +00:00
|
|
|
#ifdef SK_EDITOR_DEBUG_OUT
|
2019-05-03 21:14:21 +00:00
|
|
|
debug_on_key(key, state, modifiers);
|
2019-06-20 15:29:10 +00:00
|
|
|
#endif // SK_EDITOR_DEBUG_OUT
|
2019-04-28 18:40:45 +00:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
2019-04-24 17:56:16 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
struct EditorApplication : public sk_app::Application {
|
|
|
|
std::unique_ptr<sk_app::Window> fWindow;
|
2019-04-28 18:40:45 +00:00
|
|
|
EditorLayer fLayer;
|
2019-04-24 17:56:16 +00:00
|
|
|
|
|
|
|
EditorApplication(const char* path, void* platformData)
|
2019-04-28 18:40:45 +00:00
|
|
|
: fWindow(sk_app::Window::CreateNativeWindow(platformData))
|
2019-04-24 17:56:16 +00:00
|
|
|
{
|
2019-04-28 18:40:45 +00:00
|
|
|
//sk_app::Window::BackendType backendType = sk_app::Window::kRaster_BackendType;
|
|
|
|
sk_app::Window::BackendType backendType = sk_app::Window::kNativeGL_BackendType;
|
|
|
|
fWindow->attach(backendType);
|
2019-05-03 21:14:21 +00:00
|
|
|
fLayer.inval();
|
2019-04-28 18:40:45 +00:00
|
|
|
fLayer.loadFile(path);
|
|
|
|
fWindow->pushLayer(&fLayer);
|
2019-06-26 16:11:31 +00:00
|
|
|
fWindow->setTitle(SkStringPrintf("Editor: \"%s\"", fLayer.fPath.c_str()).c_str());
|
2019-04-24 17:56:16 +00:00
|
|
|
fWindow->show();
|
2019-04-28 18:40:45 +00:00
|
|
|
fLayer.onResize(fWindow->width(), fWindow->height());
|
2019-06-20 15:29:10 +00:00
|
|
|
#ifdef SK_EDITOR_DEBUG_OUT
|
2019-05-28 16:01:06 +00:00
|
|
|
Timer timer("shaping");
|
2019-06-20 15:29:10 +00:00
|
|
|
#endif // SK_EDITOR_DEBUG_OUT
|
2019-05-28 16:01:06 +00:00
|
|
|
fLayer.fEditor.paint(nullptr, editor::Editor::PaintOpts());
|
2019-04-24 17:56:16 +00:00
|
|
|
}
|
|
|
|
~EditorApplication() override { fWindow->detach(); }
|
|
|
|
|
|
|
|
void onIdle() override {}
|
|
|
|
};
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
sk_app::Application* sk_app::Application::Create(int argc, char** argv, void* dat) {
|
2019-04-28 18:40:45 +00:00
|
|
|
return new EditorApplication(argc > 1 ? argv[1] : nullptr, dat);
|
2019-04-24 17:56:16 +00:00
|
|
|
}
|