skia2/experimental/editor/editor_application.cpp
Hal Canary 5fd38c0959 experimental/editor/application: pre-shape text before showing window
Also, make constructor do even less work.

Change-Id: Iced41488757d6d5d01c4e1d02eee03ab2bb109a4
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/234216
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Ben Wagner <bungeman@google.com>
Auto-Submit: Hal Canary <halcanary@google.com>
2019-08-13 14:08:09 +00:00

389 lines
15 KiB
C++

// 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
#include "include/core/SkCanvas.h"
#include "include/core/SkSurface.h"
#include "include/core/SkTime.h"
#include "tools/ModifierKey.h"
#include "tools/sk_app/Application.h"
#include "tools/sk_app/Window.h"
#include "experimental/editor/editor.h"
#include "third_party/icu/SkLoadICU.h"
#include <fstream>
#include <memory>
#ifdef SK_EDITOR_DEBUG_OUT
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 "?";
}
}
static SkString modifiers_desc(ModifierKey m) {
SkString s;
#define M(X) if (m & ModifierKey::k ## X ##) { s.append(" {" #X "}"); }
M(Shift) M(Control) M(Option) M(Command) M(FirstPress)
#undef M
return s;
}
static void debug_on_char(SkUnichar c, ModifierKey modifiers) {
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());
}
}
static void debug_on_key(sk_app::Window::Key key, InputState, ModifierKey modi) {
SkDebugf("key: %s%s\n", key_name(key), modifiers_desc(modi).c_str());
}
#endif // SK_EDITOR_DEBUG_OUT
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;
}
}
namespace {
struct Timer {
double fTime;
const char* fDesc;
Timer(const char* desc = "") : fTime(SkTime::GetNSecs()), fDesc(desc) {}
~Timer() { SkDebugf("%s: %5d μs\n", fDesc, (int)((SkTime::GetNSecs() - fTime) * 1e-3)); }
};
struct EditorLayer : public sk_app::Window::Layer {
SkString fPath;
sk_app::Window* fParent = nullptr;
// TODO(halcanary): implement a cross-platform clipboard interface.
std::vector<char> fClipboard;
editor::Editor fEditor;
editor::Editor::TextPosition fTextPos{0, 0};
editor::Editor::TextPosition fMarkPos;
int fPos = 0; // window pixel position in file
int fWidth = 0; // window width
int fHeight = 0; // window height
int fMargin = 10;
bool fShiftDown = false;
bool fBlink = false;
bool fMouseDown = false;
void loadFile(const char* path) {
if (sk_sp<SkData> data = SkData::MakeFromFileName(path)) {
fPath = path;
fEditor.insert(editor::Editor::TextPosition{0, 0},
(const char*)data->data(), data->size());
} else {
fPath = "output.txt";
}
}
void onPaint(SkSurface* surface) override {
SkCanvas* canvas = surface->getCanvas();
SkAutoCanvasRestore acr(canvas, true);
canvas->clipRect({0, 0, (float)fWidth, (float)fHeight});
canvas->translate(fMargin, (float)(fMargin - fPos));
editor::Editor::PaintOpts options;
options.fCursor = fTextPos;
options.fCursorColor = {1, 0, 0, fBlink ? 0.0f : 1.0f};
options.fBackgroundColor = SkColor4f{0.8f, 0.8f, 0.8f, 1};
options.fCursorColor = {1, 0, 0, fBlink ? 0.0f : 1.0f};
if (fMarkPos != editor::Editor::TextPosition()) {
options.fSelectionBegin = fMarkPos;
options.fSelectionEnd = fTextPos;
}
#ifdef SK_EDITOR_DEBUG_OUT
{
Timer timer("shaping");
fEditor.paint(nullptr, options);
}
Timer timer("painting");
#endif // SK_EDITOR_DEBUG_OUT
fEditor.paint(canvas, options);
}
void onResize(int width, int height) override {
if (SkISize{fWidth, fHeight} != SkISize{width, height}) {
fHeight = height;
if (width != fWidth) {
fWidth = width;
fEditor.setWidth(fWidth - 2 * fMargin);
}
this->inval();
}
}
void onAttach(sk_app::Window* w) override { fParent = w; }
bool scroll(int delta) {
int maxPos = std::max(0, fEditor.getHeight() + 2 * fMargin - fHeight / 2);
int newpos = std::max(0, std::min(fPos + delta, maxPos));
if (newpos != fPos) {
fPos = newpos;
this->inval();
}
return true;
}
void inval() { if (fParent) { fParent->inval(); } }
bool onMouseWheel(float delta, ModifierKey) override {
this->scroll(-(int)(delta * fEditor.font().getSpacing()));
return true;
}
bool onMouse(int x, int y, InputState state, ModifierKey modifiers) override {
bool mouseDown = InputState::kDown == state;
if (mouseDown) {
fMouseDown = true;
} else if (InputState::kUp == state) {
fMouseDown = false;
}
bool shiftOrDrag = skstd::Any(modifiers & ModifierKey::kShift) || !mouseDown;
if (fMouseDown) {
return this->move(fEditor.getPosition({x - fMargin, y + fPos - fMargin}), shiftOrDrag);
}
return false;
}
bool onChar(SkUnichar c, ModifierKey modi) override {
using skstd::Any;
modi &= ~ModifierKey::kFirstPress;
if (!Any(modi & (ModifierKey::kControl |
ModifierKey::kOption |
ModifierKey::kCommand))) {
if (((unsigned)c < 0x7F && (unsigned)c >= 0x20) || c == '\n') {
char ch = (char)c;
fEditor.insert(fTextPos, &ch, 1);
#ifdef SK_EDITOR_DEBUG_OUT
SkDebugf("insert: %X'%c'\n", (unsigned)c, ch);
#endif // SK_EDITOR_DEBUG_OUT
return this->moveCursor(editor::Editor::Movement::kRight);
}
}
static constexpr ModifierKey kCommandOrControl = ModifierKey::kCommand |
ModifierKey::kControl;
if (Any(modi & kCommandOrControl) && !Any(modi & ~kCommandOrControl)) {
switch (c) {
case 'p':
for (editor::StringView str : fEditor.text()) {
SkDebugf(">> '%.*s'\n", str.size, str.data);
}
return true;
case 's':
{
std::ofstream out(fPath.c_str());
size_t count = fEditor.lineCount();
for (size_t i = 0; i < count; ++i) {
if (i != 0) {
out << '\n';
}
editor::StringView str = fEditor.line(i);
out.write(str.data, str.size);
}
}
return true;
case 'c':
if (fMarkPos != editor::Editor::TextPosition()) {
fClipboard.resize(fEditor.copy(fMarkPos, fTextPos, nullptr));
fEditor.copy(fMarkPos, fTextPos, fClipboard.data());
return true;
}
case 'x':
if (fMarkPos != editor::Editor::TextPosition()) {
fClipboard.resize(fEditor.copy(fMarkPos, fTextPos, nullptr));
fEditor.copy(fMarkPos, fTextPos, fClipboard.data());
(void)this->move(fEditor.remove(fMarkPos, fTextPos), false);
this->inval();
return true;
}
case 'v':
if (fClipboard.size()) {
fEditor.insert(fTextPos, fClipboard.data(), fClipboard.size());
this->inval();
return true;
}
}
}
#ifdef SK_EDITOR_DEBUG_OUT
debug_on_char(c, modifiers);
#endif // SK_EDITOR_DEBUG_OUT
return false;
}
bool moveCursor(editor::Editor::Movement m, bool shift = false) {
return this->move(fEditor.move(m, fTextPos), shift);
}
bool move(editor::Editor::TextPosition pos, bool shift) {
if (pos == fTextPos || pos == editor::Editor::TextPosition()) {
if (!shift) {
fMarkPos = editor::Editor::TextPosition();
}
return false;
}
if (shift != fShiftDown) {
fMarkPos = shift ? fTextPos : editor::Editor::TextPosition();
fShiftDown = shift;
}
fTextPos = pos;
// scroll if needed.
SkIRect cursor = fEditor.getLocation(fTextPos).roundOut();
if (fPos < cursor.bottom() - fHeight + 2 * fMargin) {
fPos = cursor.bottom() - fHeight + 2 * fMargin;
} else if (cursor.top() < fPos) {
fPos = cursor.top();
}
this->inval();
return true;
}
bool onKey(sk_app::Window::Key key,
InputState state,
ModifierKey modifiers) override {
if (state != InputState::kDown) {
return false; // ignore keyup
}
// ignore other modifiers.
using skstd::Any;
ModifierKey ctrlAltCmd = modifiers & (ModifierKey::kControl |
ModifierKey::kOption |
ModifierKey::kCommand);
bool shift = Any(modifiers & (ModifierKey::kShift));
if (!Any(ctrlAltCmd)) {
// no modifiers
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), shift);
case sk_app::Window::Key::kDelete:
if (fMarkPos != editor::Editor::TextPosition()) {
(void)this->move(fEditor.remove(fMarkPos, fTextPos), false);
} else {
auto pos = fEditor.move(editor::Editor::Movement::kRight, fTextPos);
(void)this->move(fEditor.remove(fTextPos, pos), false);
}
this->inval();
return true;
case sk_app::Window::Key::kBack:
if (fMarkPos != editor::Editor::TextPosition()) {
(void)this->move(fEditor.remove(fMarkPos, fTextPos), false);
} else {
auto pos = fEditor.move(editor::Editor::Movement::kLeft, fTextPos);
(void)this->move(fEditor.remove(fTextPos, pos), false);
}
this->inval();
return true;
case sk_app::Window::Key::kOK:
return this->onChar('\n', modifiers);
default:
break;
}
} else if (skstd::Any(ctrlAltCmd & (ModifierKey::kControl | ModifierKey::kCommand))) {
switch (key) {
case sk_app::Window::Key::kLeft:
return this->moveCursor(editor::Editor::Movement::kWordLeft, shift);
case sk_app::Window::Key::kRight:
return this->moveCursor(editor::Editor::Movement::kWordRight, shift);
default:
break;
}
}
#ifdef SK_EDITOR_DEBUG_OUT
debug_on_key(key, state, modifiers);
#endif // SK_EDITOR_DEBUG_OUT
return false;
}
};
static constexpr float kFontSize = 18;
// static constexpr char kTypefaceName[] = "monospace";
static constexpr char kTypefaceName[] = "sans-serif";
static constexpr SkFontStyle::Weight kFontWeight = SkFontStyle::kNormal_Weight;
static constexpr SkFontStyle::Width kFontWidth = SkFontStyle::kNormal_Width;
static constexpr SkFontStyle::Slant kFontSlant = SkFontStyle::kUpright_Slant;
//static constexpr sk_app::Window::BackendType kBackendType = sk_app::Window::kRaster_BackendType;
static constexpr sk_app::Window::BackendType kBackendType = sk_app::Window::kNativeGL_BackendType;
struct EditorApplication : public sk_app::Application {
std::unique_ptr<sk_app::Window> fWindow;
EditorLayer fLayer;
double fNextTime = -DBL_MAX;
EditorApplication(std::unique_ptr<sk_app::Window> win) : fWindow(std::move(win)) {}
bool init(const char* path) {
fWindow->attach(kBackendType);
fLayer.fEditor.setFont(SkFont(SkTypeface::MakeFromName(kTypefaceName,
SkFontStyle(kFontWeight, kFontWidth, kFontSlant)), kFontSize));
fLayer.loadFile(path);
fWindow->pushLayer(&fLayer);
fWindow->setTitle(SkStringPrintf("Editor: \"%s\"", fLayer.fPath.c_str()).c_str());
fLayer.onResize(fWindow->width(), fWindow->height());
fLayer.fEditor.paint(nullptr, editor::Editor::PaintOpts());
fWindow->show();
return true;
}
~EditorApplication() override { fWindow->detach(); }
void onIdle() override {
double now = SkTime::GetNSecs();
if (now >= fNextTime) {
constexpr double kHalfPeriodNanoSeconds = 0.5 * 1e9;
fNextTime = now + kHalfPeriodNanoSeconds;
fLayer.fBlink = !fLayer.fBlink;
fWindow->inval();
}
}
};
} // namespace
sk_app::Application* sk_app::Application::Create(int argc, char** argv, void* dat) {
if (!SkLoadICU()) {
SK_ABORT("SkLoadICU failed.");
}
std::unique_ptr<sk_app::Window> win(sk_app::Window::CreateNativeWindow(dat));
if (!win) {
SK_ABORT("CreateNativeWindow failed.");
}
std::unique_ptr<EditorApplication> app(new EditorApplication(std::move(win)));
(void)app->init(argc > 1 ? argv[1] : nullptr);
return app.release();
}