428 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C++
		
	
	
	
			
		
		
	
	
			428 lines
		
	
	
		
			16 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.
 | |
| 
 | |
| // 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/sk_app/Application.h"
 | |
| #include "tools/sk_app/Window.h"
 | |
| #include "tools/skui/ModifierKey.h"
 | |
| 
 | |
| #include "modules/skplaintexteditor/include/editor.h"
 | |
| 
 | |
| #include "third_party/icu/SkLoadICU.h"
 | |
| 
 | |
| #include <fstream>
 | |
| #include <memory>
 | |
| 
 | |
| using SkPlainTextEditor::Editor;
 | |
| using SkPlainTextEditor::StringView;
 | |
| 
 | |
| #ifdef SK_EDITOR_DEBUG_OUT
 | |
| static const char* key_name(skui::Key k) {
 | |
|     switch (k) {
 | |
|         #define M(X) case skui::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(skui::ModifierKey m) {
 | |
|     SkString s;
 | |
|     #define M(X) if (m & skui::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, skui::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(skui::Key key, skui::InputState, skui::ModifierKey modi) {
 | |
|     SkDebugf("key: %s%s\n", key_name(key), modifiers_desc(modi).c_str());
 | |
| }
 | |
| #endif  // SK_EDITOR_DEBUG_OUT
 | |
| 
 | |
| static Editor::Movement convert(skui::Key key) {
 | |
|     switch (key) {
 | |
|         case skui::Key::kLeft:  return Editor::Movement::kLeft;
 | |
|         case skui::Key::kRight: return Editor::Movement::kRight;
 | |
|         case skui::Key::kUp:    return Editor::Movement::kUp;
 | |
|         case skui::Key::kDown:  return Editor::Movement::kDown;
 | |
|         case skui::Key::kHome:  return Editor::Movement::kHome;
 | |
|         case skui::Key::kEnd:   return Editor::Movement::kEnd;
 | |
|         default: return 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)); }
 | |
| };
 | |
| 
 | |
| static constexpr float kFontSize = 18;
 | |
| static const char* kTypefaces[3] = {"sans-serif", "serif", "monospace"};
 | |
| static constexpr size_t kTypefaceCount = SK_ARRAY_COUNT(kTypefaces);
 | |
| 
 | |
| static constexpr SkFontStyle::Weight kFontWeight = SkFontStyle::kNormal_Weight;
 | |
| static constexpr SkFontStyle::Width  kFontWidth  = SkFontStyle::kNormal_Width;
 | |
| static constexpr SkFontStyle::Slant  kFontSlant  = SkFontStyle::kUpright_Slant;
 | |
| 
 | |
| 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 fEditor;
 | |
|     Editor::TextPosition fTextPos{0, 0};
 | |
|     Editor::TextPosition fMarkPos;
 | |
|     int fPos = 0;  // window pixel position in file
 | |
|     int fWidth = 0;  // window width
 | |
|     int fHeight = 0;  // window height
 | |
|     int fMargin = 10;
 | |
|     size_t fTypefaceIndex = 0;
 | |
|     float fFontSize = kFontSize;
 | |
|     bool fShiftDown = false;
 | |
|     bool fBlink = false;
 | |
|     bool fMouseDown = false;
 | |
| 
 | |
|     void setFont() {
 | |
|         fEditor.setFont(SkFont(SkTypeface::MakeFromName(kTypefaces[fTypefaceIndex],
 | |
|                                SkFontStyle(kFontWeight, kFontWidth, kFontSlant)), fFontSize));
 | |
|     }
 | |
| 
 | |
| 
 | |
|     void loadFile(const char* path) {
 | |
|         if (sk_sp<SkData> data = SkData::MakeFromFileName(path)) {
 | |
|             fPath = path;
 | |
|             fEditor.insert(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::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::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, skui::ModifierKey) override {
 | |
|         this->scroll(-(int)(delta * fEditor.font().getSpacing()));
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     bool onMouse(int x, int y, skui::InputState state, skui::ModifierKey modifiers) override {
 | |
|         bool mouseDown = skui::InputState::kDown == state;
 | |
|         if (mouseDown) {
 | |
|             fMouseDown = true;
 | |
|         } else if (skui::InputState::kUp == state) {
 | |
|             fMouseDown = false;
 | |
|         }
 | |
|         bool shiftOrDrag = sknonstd::Any(modifiers & skui::ModifierKey::kShift) || !mouseDown;
 | |
|         if (fMouseDown) {
 | |
|             return this->move(fEditor.getPosition({x - fMargin, y + fPos - fMargin}), shiftOrDrag);
 | |
|         }
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     bool onChar(SkUnichar c, skui::ModifierKey modi) override {
 | |
|         using sknonstd::Any;
 | |
|         modi &= ~skui::ModifierKey::kFirstPress;
 | |
|         if (!Any(modi & (skui::ModifierKey::kControl |
 | |
|                          skui::ModifierKey::kOption  |
 | |
|                          skui::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::Movement::kRight);
 | |
|             }
 | |
|         }
 | |
|         static constexpr skui::ModifierKey kCommandOrControl = skui::ModifierKey::kCommand |
 | |
|                                                                skui::ModifierKey::kControl;
 | |
|         if (Any(modi & kCommandOrControl) && !Any(modi & ~kCommandOrControl)) {
 | |
|             switch (c) {
 | |
|                 case 'p':
 | |
|                     for (StringView str : fEditor.text()) {
 | |
|                         SkDebugf(">>  '%.*s'\n", (int)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';
 | |
|                             }
 | |
|                             StringView str = fEditor.line(i);
 | |
|                             out.write(str.data, str.size);
 | |
|                         }
 | |
|                     }
 | |
|                     return true;
 | |
|                 case 'c':
 | |
|                     if (fMarkPos != Editor::TextPosition()) {
 | |
|                         fClipboard.resize(fEditor.copy(fMarkPos, fTextPos, nullptr));
 | |
|                         fEditor.copy(fMarkPos, fTextPos, fClipboard.data());
 | |
|                         return true;
 | |
|                     }
 | |
|                     return false;
 | |
|                 case 'x':
 | |
|                     if (fMarkPos != 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;
 | |
|                     }
 | |
|                     return false;
 | |
|                 case 'v':
 | |
|                     if (fClipboard.size()) {
 | |
|                         fEditor.insert(fTextPos, fClipboard.data(), fClipboard.size());
 | |
|                         this->inval();
 | |
|                         return true;
 | |
|                     }
 | |
|                     return false;
 | |
|                 case '0':
 | |
|                     fTypefaceIndex = (fTypefaceIndex + 1) % kTypefaceCount;
 | |
|                     this->setFont();
 | |
|                     return true;
 | |
|                 case '=':
 | |
|                 case '+':
 | |
|                     fFontSize = fFontSize + 1;
 | |
|                     this->setFont();
 | |
|                     return true;
 | |
|                 case '-':
 | |
|                 case '_':
 | |
|                     if (fFontSize > 1) {
 | |
|                         fFontSize = fFontSize - 1;
 | |
|                         this->setFont();
 | |
|                     }
 | |
|             }
 | |
|         }
 | |
|         #ifdef SK_EDITOR_DEBUG_OUT
 | |
|         debug_on_char(c, modifiers);
 | |
|         #endif  // SK_EDITOR_DEBUG_OUT
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     bool moveCursor(Editor::Movement m, bool shift = false) {
 | |
|         return this->move(fEditor.move(m, fTextPos), shift);
 | |
|     }
 | |
| 
 | |
|     bool move(Editor::TextPosition pos, bool shift) {
 | |
|         if (pos == fTextPos || pos == Editor::TextPosition()) {
 | |
|             if (!shift) {
 | |
|                 fMarkPos = Editor::TextPosition();
 | |
|             }
 | |
|             return false;
 | |
|         }
 | |
|         if (shift != fShiftDown) {
 | |
|             fMarkPos = shift ? fTextPos : 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(skui::Key key,
 | |
|                skui::InputState state,
 | |
|                skui::ModifierKey modifiers) override {
 | |
|         if (state != skui::InputState::kDown) {
 | |
|             return false;  // ignore keyup
 | |
|         }
 | |
|         // ignore other modifiers.
 | |
|         using sknonstd::Any;
 | |
|         skui::ModifierKey ctrlAltCmd = modifiers & (skui::ModifierKey::kControl |
 | |
|                                               skui::ModifierKey::kOption  |
 | |
|                                               skui::ModifierKey::kCommand);
 | |
|         bool shift = Any(modifiers & (skui::ModifierKey::kShift));
 | |
|         if (!Any(ctrlAltCmd)) {
 | |
|             // no modifiers
 | |
|             switch (key) {
 | |
|                 case skui::Key::kPageDown:
 | |
|                     return this->scroll(fHeight * 4 / 5);
 | |
|                 case skui::Key::kPageUp:
 | |
|                     return this->scroll(-fHeight * 4 / 5);
 | |
|                 case skui::Key::kLeft:
 | |
|                 case skui::Key::kRight:
 | |
|                 case skui::Key::kUp:
 | |
|                 case skui::Key::kDown:
 | |
|                 case skui::Key::kHome:
 | |
|                 case skui::Key::kEnd:
 | |
|                     return this->moveCursor(convert(key), shift);
 | |
|                 case skui::Key::kDelete:
 | |
|                     if (fMarkPos != Editor::TextPosition()) {
 | |
|                         (void)this->move(fEditor.remove(fMarkPos, fTextPos), false);
 | |
|                     } else {
 | |
|                         auto pos = fEditor.move(Editor::Movement::kRight, fTextPos);
 | |
|                         (void)this->move(fEditor.remove(fTextPos, pos), false);
 | |
|                     }
 | |
|                     this->inval();
 | |
|                     return true;
 | |
|                 case skui::Key::kBack:
 | |
|                     if (fMarkPos != Editor::TextPosition()) {
 | |
|                         (void)this->move(fEditor.remove(fMarkPos, fTextPos), false);
 | |
|                     } else {
 | |
|                         auto pos = fEditor.move(Editor::Movement::kLeft, fTextPos);
 | |
|                         (void)this->move(fEditor.remove(fTextPos, pos), false);
 | |
|                     }
 | |
|                     this->inval();
 | |
|                     return true;
 | |
|                 case skui::Key::kOK:
 | |
|                     return this->onChar('\n', modifiers);
 | |
|                 default:
 | |
|                     break;
 | |
|             }
 | |
|         } else if (sknonstd::Any(ctrlAltCmd & (skui::ModifierKey::kControl |
 | |
|                                                skui::ModifierKey::kCommand))) {
 | |
|             switch (key) {
 | |
|                 case skui::Key::kLeft:
 | |
|                     return this->moveCursor(Editor::Movement::kWordLeft, shift);
 | |
|                 case skui::Key::kRight:
 | |
|                     return this->moveCursor(Editor::Movement::kWordRight, shift);
 | |
|                 default:
 | |
|                     break;
 | |
|             }
 | |
|         }
 | |
|         #ifdef SK_EDITOR_DEBUG_OUT
 | |
|         debug_on_key(key, state, modifiers);
 | |
|         #endif  // SK_EDITOR_DEBUG_OUT
 | |
|         return false;
 | |
|     }
 | |
| };
 | |
| 
 | |
| #ifdef SK_VULKAN
 | |
| static constexpr sk_app::Window::BackendType kBackendType = sk_app::Window::kVulkan_BackendType;
 | |
| #elif SK_METAL
 | |
| static constexpr sk_app::Window::BackendType kBackendType = sk_app::Window::kMetal_BackendType;
 | |
| #elif SK_GL
 | |
| static constexpr sk_app::Window::BackendType kBackendType = sk_app::Window::kNativeGL_BackendType;
 | |
| #elif SK_DAWN
 | |
| static constexpr sk_app::Window::BackendType kBackendType = sk_app::Window::kDawn_BackendType;
 | |
| #else
 | |
| static constexpr sk_app::Window::BackendType kBackendType = sk_app::Window::kRaster_BackendType;
 | |
| #endif
 | |
| 
 | |
| 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.loadFile(path);
 | |
|         fLayer.setFont();
 | |
| 
 | |
|         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::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();
 | |
| }
 |