From c5c23d417677ce33f8f0966466e455fad149d496 Mon Sep 17 00:00:00 2001 From: John Sennesael Date: Sun, 29 Aug 2021 12:55:50 -0500 Subject: [PATCH] Add mouse handling + other improvements & bugfixes --- .gitignore | 1 + lib/neovision/include/neovision/application.h | 35 ++--- lib/neovision/include/neovision/term.h | 6 +- lib/neovision/include/neovision/view.h | 59 +++++++- lib/neovision/include/neovision/window.h | 8 ++ lib/neovision/src/application.cpp | 108 ++++++++------- lib/neovision/src/crt.cpp | 6 +- lib/neovision/src/term.cpp | 10 +- lib/neovision/src/view.cpp | 130 ++++++++++++++---- lib/neovision/src/window.cpp | 11 +- mkacct/include/application.h | 2 + mkacct/src/mkacct.cpp | 8 +- 12 files changed, 268 insertions(+), 116 deletions(-) diff --git a/.gitignore b/.gitignore index 784cea4..990510e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ *.sw* mkacct/build/* lib/neovision/build/* +lib/neovision/doc/* diff --git a/lib/neovision/include/neovision/application.h b/lib/neovision/include/neovision/application.h index 7a0573c..396bec5 100644 --- a/lib/neovision/include/neovision/application.h +++ b/lib/neovision/include/neovision/application.h @@ -26,10 +26,12 @@ class Application: public View { wchar_t m_bgFillCharacter{L'░'}; int m_ttyFd{0}; + InputParser m_inputParser; bool m_running{false}; std::shared_ptr m_selfPtr; Terminal m_terminal; - std::vector> m_views; + + void InitializeViews(); public: @@ -44,14 +46,11 @@ public: virtual ~Application(); /** - * @brief Adds a view to the application. + * @brief Initializes the application for use. * - * The application will take ownership of the view and render it when - * appropriate etc,... - * - * @param view View to add. + * You must call this before calling Run(). */ - virtual void Add(std::unique_ptr view); + virtual void Initialize(); /** * @brief Starts the application. @@ -73,26 +72,22 @@ public: protected: /** - * @brief Recalculates the z-order of internal views. + * @brief Mouse event. * - * Called automatically by views when their z-order changes, or when new - * views are added. + * Triggers when a mouse event happens. + * + * @param[in] mouseData Mouse event data gets passed in here. */ - void CalculateZOrders(); + virtual void OnMouse(const MouseEventData& mouseData); /** - * @brief Initialize event. + * @brief Key event. * - * Initializes all views when Run() is called. Views added after Run() is - * called get initialized as they get added. + * Triggers when a keyboard event happens. * + * @param[in] keyData Received keyboard input. */ - virtual void Initialize(); - - /** - * @brief Application draw event. - */ - virtual void OnDraw(); + virtual void OnKey(const std::wstring& keyData); /** * @brief Application resize event. diff --git a/lib/neovision/include/neovision/term.h b/lib/neovision/include/neovision/term.h index aa28bee..2f33990 100644 --- a/lib/neovision/include/neovision/term.h +++ b/lib/neovision/include/neovision/term.h @@ -41,7 +41,7 @@ class Terminal { TerminalEventCallback m_onResize; TerminalEventCallback m_onTerminate; struct sigaction m_resizeSignal{}; - std::chrono::seconds m_ioTimeOut{1}; + std::chrono::milliseconds m_ioTimeOut{100}; int m_fd{0}; static void sigIntHandler(int); @@ -129,14 +129,14 @@ public: * * @return Returns the current I/O timeout. */ - std::chrono::seconds Timeout() const; + std::chrono::milliseconds Timeout() const; /** * @brief Set IO timeout. * * @param[in] t New IO timeout in seconds. */ - void Timeout(const std::chrono::seconds& t); + void Timeout(const std::chrono::milliseconds& t); /** * @brief Disables buffered i/o in the terminal. diff --git a/lib/neovision/include/neovision/view.h b/lib/neovision/include/neovision/view.h index f1b0b15..d9bdd4f 100644 --- a/lib/neovision/include/neovision/view.h +++ b/lib/neovision/include/neovision/view.h @@ -34,11 +34,14 @@ class View Vector2D m_cursorPosition; bool m_dirty{true}; + bool m_initialized{false}; + std::optional> m_parent{}; Vector2D m_position; std::mutex m_positionMutex; ViewBuffer m_previousBuffer; std::mutex m_previousBufferMutex; Vector2D m_size; + std::vector> m_views; std::atomic m_zOrder{0}; void DoRender( @@ -57,6 +60,24 @@ public: */ virtual ~View(); + /** + * @brief Adds a child view. + * + * This view will take ownership of the child and render it when + * appropriate etc,... + * + * @param view View to add. + */ + virtual void Add(std::unique_ptr view); + + /** + * @brief Recalculates the z-order of child views. + * + * Called automatically by views when their z-order changes, or when new + * views are added. + */ + void CalculateZOrders(); + /** * @brief Get cursor position. * @@ -93,6 +114,20 @@ public: */ bool Dirty() const; + /** + * @brief Check initialized. + * + * @return Returns true if the view has been initialized. + */ + bool Initialized() const; + + /** + * @brief Get parent. + * + * @return Returns an observing pointer to the parent view if there is one. + */ + std::optional> Parent(); + /** * @brief Set position. * @@ -127,8 +162,12 @@ public: * @brief Set dirty flag true. * * Forces the view to render next render event. + * + * @param[in] propagateChildren Marks all child views as dirty as well if + * true. + * @param[in] propagateParent Marks parent as dirty as well if true. */ - void SetDirty(); + void SetDirty(bool propagateChildren = true, bool propagateParent = true); /** * @brief Set size. @@ -211,7 +250,7 @@ protected: * * This runs when the application initializes the view. */ - virtual void Initialize() = 0; + virtual void Initialize(); /** * @brief Draw event. @@ -223,7 +262,21 @@ protected: * * Must be implemented when creating a view. */ - virtual void OnDraw() = 0; + virtual void OnDraw(); + + /** + * @brief Key event. + * + * Triggers when a keyboard event happens. + */ + virtual void OnKey(const std::wstring&){}; + + /** + * @brief Mouse event. + * + * Triggers when a mouse event happens. + */ + virtual void OnMouse(const MouseEventData&){}; /** * @brief Write to the internal view buffer. diff --git a/lib/neovision/include/neovision/window.h b/lib/neovision/include/neovision/window.h index dc9ea22..0d79d4f 100644 --- a/lib/neovision/include/neovision/window.h +++ b/lib/neovision/include/neovision/window.h @@ -15,6 +15,7 @@ class Window: public View { std::wstring m_title{L"Window"}; + std::wstring m_content; public: @@ -58,6 +59,13 @@ protected: */ virtual void OnDraw(); + /** + * @brief Mouse event. + * + * Triggers when a mouse event happens. + */ + virtual void OnMouse(const MouseEventData&); + }; } // namespace neovision diff --git a/lib/neovision/src/application.cpp b/lib/neovision/src/application.cpp index 5a773f5..e463d84 100644 --- a/lib/neovision/src/application.cpp +++ b/lib/neovision/src/application.cpp @@ -1,4 +1,5 @@ #include +#include #include #include "neovision/ansi.h" @@ -25,64 +26,62 @@ Application::~Application() if (m_running == true) Stop(); } -void Application::Add(std::unique_ptr view) -{ - View& viewRef = *view; - m_views.emplace_back(std::move(view)); - CalculateZOrders(); - if (m_running == true) viewRef.Initialize(); -} - -void Application::CalculateZOrders() -{ - std::sort(m_views.begin(), m_views.end(), - [](const std::unique_ptr& a, const std::unique_ptr& b){ - return a->Zorder() < b->Zorder(); - } - ); -} - void Application::Initialize() { + View::Initialize(); m_terminal.Initialize(); - IO::Get().SetInputFunction([&](size_t b){ + IO::Get().SetInputFunction([this](size_t b){ return m_terminal.Read(b); }); - IO::Get().SetOutputFunction([&](const std::wstring& data){ + IO::Get().SetOutputFunction([this](const std::wstring& data){ m_terminal.Write(data); }); - for (auto& view: m_views) - { - view->Initialize(); - } -} - -void Application::Run() -{ - ClearScreen(); - m_running = true; m_terminal.Unbuffer(); Size(m_terminal.Size()- Vector2D(1, 1), m_bgFillCharacter); m_terminal.SetOnResize([&](){ const Vector2D newSize = m_terminal.Size() - Vector2D(1, 1); Size(newSize, m_bgFillCharacter); - for (const auto& v: m_views) - { - v->SetDirty(); - } OnResize(newSize); }); m_terminal.SetOnTerminate([&](){ Stop(); }); SetMouseReporting(MouseEventMode::ButtonOnly); - Initialize(); - InputParser inputParser; + m_inputParser.SetOnKeyEvent([this](const std::wstring& k){ + OnKey(k); + }); + m_inputParser.SetOnMouseEvent([this](const MouseEventData& m){ + OnMouse(m); + }); + InitializeViews(); +} + +void Application::InitializeViews() +{ + for (auto& view: m_views) + { + if (!view) return; + if (view->Initialized() != true) view->Initialize(); + } +} + +void Application::Run() +{ + if (!Initialized()) + { + throw std::runtime_error( + "Application::Run() called before " + "Application::Initialized() was called." + ); + } + ClearScreen(); + m_running = true; + InitializeViews(); while (m_running) { m_terminal.ProcessEvents(); OnDraw(); - inputParser.Read(); + m_inputParser.Read(); } } @@ -99,28 +98,33 @@ Terminal& Application::Term() return m_terminal; } -void Application::OnDraw() +void Application::OnMouse(const MouseEventData& mouseData) { - // Let all views render themselves first. - for (auto& v: m_views) + /* Figure out what view is clicked. Reverse iterate so the top-most window + always receives the click (the array should be sorted in z-order) */ + for (auto viewIter = m_views.rbegin(); viewIter != m_views.rend(); + ++viewIter) { - if (v->Dirty() == true) + auto& view = (*viewIter); + const std::uint32_t x1 = view->Position().X(); + const std::uint32_t x2 = x1 + view->Size().X(); + const std::uint32_t y1 = view->Position().Y(); + const std::uint32_t y2 = y1 + view->Size().Y(); + if ( + (mouseData.position.X() >= x1) + && (mouseData.position.X() <= x2) + && (mouseData.position.Y() >= y1) + && (mouseData.position.Y() <= y2) ) { - v->OnDraw(); + view->OnMouse(mouseData); + break; } } +} - // Render all views (they should be sorted by z-order already) onto ourself. - for (auto& v: m_views) - { - if ((v->Dirty() == true) || (m_dirty == true)) - { - v->Render(*this); - } - } - - // Now we render everything to the screen. - Render(); +void Application::OnKey(const std::wstring& keyData) +{ + if (keyData.empty()) return; } } // namespace neovision diff --git a/lib/neovision/src/crt.cpp b/lib/neovision/src/crt.cpp index a4b57f2..460c97d 100644 --- a/lib/neovision/src/crt.cpp +++ b/lib/neovision/src/crt.cpp @@ -281,7 +281,6 @@ void InputParser::Read() /* If we didn't get a mouse event, just fire a key event with the data we have. */ if (m_onKeyEvent) m_onKeyEvent(ansiParser.ProcessedCharacters()); - } void InputParser::SetOnKeyEvent(const KeyboardEventCallback& ev) @@ -430,6 +429,11 @@ void SetMouseReporting( std::wstring{ansi::MouseTrackMode::VT200} + ansi::DecPmSuffix::DISABLE }); + outStr += ansi::MakeCsiSequence({ + ansi::CsiSequence::DECPM + + std::wstring{ansi::MouseTrackMode::BTN_EVENT} + + ansi::DecPmSuffix::DISABLE + }); outStr += ansi::MakeCsiSequence({ ansi::CsiSequence::DECPM + std::wstring{ansi::MouseTrackMode::VT200_HIGHLIGHT} + diff --git a/lib/neovision/src/term.cpp b/lib/neovision/src/term.cpp index 44ee52b..3ce600e 100644 --- a/lib/neovision/src/term.cpp +++ b/lib/neovision/src/term.cpp @@ -79,6 +79,7 @@ void Terminal::sigWinchHandler(int) void Terminal::ProcessEvents() { if (!m_initialized) return; + if (!m_fd) return; winsize ws; if (ioctl(m_fd, TIOCGWINSZ, &ws) == -1) { @@ -100,11 +101,12 @@ std::wstring Terminal::Read(size_t n) while (bytesRead < n) { const auto now = std::chrono::system_clock::now(); - const auto deltaT = std::chrono::duration_cast( + const auto deltaT = std::chrono::duration_cast( now - startTime ); if (deltaT > m_ioTimeOut) break; - char buffer[n + 1]{}; + char buffer[n + 1]; + memset(buffer, 0, n + 1); const size_t br = read(m_fd, buffer, n); if (br == -1) { @@ -152,12 +154,12 @@ void Terminal::SetOnTerminate(TerminalEventCallback callback) m_onTerminate = callback; } -std::chrono::seconds Terminal::Timeout() const +std::chrono::milliseconds Terminal::Timeout() const { return m_ioTimeOut; } -void Terminal::Timeout(const std::chrono::seconds& t) +void Terminal::Timeout(const std::chrono::milliseconds& t) { m_ioTimeOut = t; } diff --git a/lib/neovision/src/view.cpp b/lib/neovision/src/view.cpp index 08bac8e..78af434 100644 --- a/lib/neovision/src/view.cpp +++ b/lib/neovision/src/view.cpp @@ -18,6 +18,24 @@ View::~View() m_previousBuffer.clear(); } +void View::Add(std::unique_ptr view) +{ + View& viewRef = *view; + viewRef.m_parent = *this; + m_views.emplace_back(std::move(view)); + CalculateZOrders(); + if (m_initialized == true) viewRef.Initialize(); +} + +void View::CalculateZOrders() +{ + std::sort(m_views.begin(), m_views.end(), + [](const std::unique_ptr& a, const std::unique_ptr& b){ + return a->Zorder() < b->Zorder(); + } + ); +} + Vector2D View::CursorPosition() const { return m_cursorPosition; @@ -33,6 +51,21 @@ bool View::Dirty() const return m_dirty; } +void View::Initialize() +{ + m_initialized = true; +} + +bool View::Initialized() const +{ + return m_initialized; +} + +std::optional> View::Parent() +{ + return m_parent; +} + void View::Position(const Vector2D& p) { m_position = p; @@ -57,12 +90,6 @@ void View::Render() void View::Render(View& view) { - { - const std::lock_guard previousBufferLock( - m_previousBufferMutex - ); - m_previousBuffer.clear(); - } DoRender([&](const std::wstring& output, const Vector2D& p){ view.WriteAt(p, output); }); @@ -73,48 +100,53 @@ void View::DoRender( ) { const std::lock_guard bufferLock(m_bufferMutex); - if (!m_dirty) return; + /* We try to be clever and only re-draw the areas that need updating. + Therefore, calling this when not dirty is fine, no unnecesary output + shoud happen. However, if the parent is dirty, we force a complete redraw + by clearing our previous buffer, because the real output is the parent's + buffer and we have no way of knowing the state of it's buffer. But that's + OK because when the parent get's to it's render phase, IT will do the + clever comparing thing as well, and again only output to the screen what + needs changing. */ + if (m_parent && (m_parent->get().Dirty() == true)) m_previousBuffer.clear(); for (std::uint32_t y = 0; y != m_size.Y() + 1; ++y) { std::wstring lineOut; const std::vector& line = m_buffer[y]; - size_t lastUpdatedX{0}; + bool needUpdate{false}; + /* Compare current buffer with previous buffer and only write the + differences to output. */ for (std::uint32_t x = 0; x != m_size.X() + 1; ++x) { if (x >= line.size()) break; const wchar_t newChar = line[x]; - bool needUpdate{false}; - if (m_buffer.size() == m_previousBuffer.size()) + const Vector2D pos = m_position + Vector2D(x, y); + if (y < m_previousBuffer.size()) { const std::vector& pline = m_previousBuffer[y]; - if (line.size() == pline.size()) + if (x < pline.size()) { const wchar_t oldChar = pline[x]; - if (newChar != oldChar) needUpdate = true; + if (newChar != oldChar) + { + needUpdate = true; + } } else { + /* If current buffer is wider than the previous buffer, + we have nothing to compare to, so just always write. */ needUpdate = true; } } else { + /* Similarly, if the current buffer is taller than the previous + * buffer, always write. */ needUpdate = true; } - if (needUpdate) - { - if ((x == 0) || (lastUpdatedX == (x - 1))) - { - lineOut += newChar; - } - else - { - const Vector2D pos = m_position + Vector2D(x, y); - outputFunc(lineOut, pos); - lineOut.clear(); - } - lastUpdatedX = x; - } + // Now write output if an update is needed for this character. + if (needUpdate) outputFunc(std::wstring{newChar}, pos); } if (!lineOut.empty()) { @@ -138,9 +170,49 @@ void View::DoRender( m_dirty = false; } -void View::SetDirty() +void View::OnDraw() +{ + // Let all child views draw themselves first. + for (auto& v: m_views) + { + if (!v) continue; + if (v->Initialized() != true) v->Initialize(); + if (v->Dirty() == true) + { + v->OnDraw(); + } + } + + /* Render all child views (they should be sorted by z-order already) + onto ourself. */ + for (auto& v: m_views) + { + if ((v->Dirty() == true) || (m_dirty == true)) + { + v->Render(*this); + } + } + // If we don't have a parent, render to screen. + if (!m_parent) Render(); +} + +void View::SetDirty(bool propagateChildren, bool propagateParent) { m_dirty = true; + if (propagateChildren == true) + { + for (const auto& v: m_views) + { + v->SetDirty(true, false); + } + } + if (m_parent) + { + if (propagateParent == true) + { + m_parent->get().SetDirty(); + } + } } void View::Size(const Vector2D& s, wchar_t fill) @@ -165,7 +237,7 @@ void View::Size(const Vector2D& s, wchar_t fill) line.resize(minColBufferSize); } } - m_dirty = true; + SetDirty(false, false); } Vector2D View::Size() const @@ -200,7 +272,7 @@ void View::WriteToBuffer(const Vector2D& p, const std::wstring s) // a position-write with a blank string still sets position. m_cursorPosition.X(p.X()); m_cursorPosition.Y(p.Y()); - m_dirty = true; + SetDirty(); if (s.empty()) return; /* We're taking this one character at the time, because things get complicated given that there's all sorts of ansi sequences that may move diff --git a/lib/neovision/src/window.cpp b/lib/neovision/src/window.cpp index 50a944f..feda786 100644 --- a/lib/neovision/src/window.cpp +++ b/lib/neovision/src/window.cpp @@ -13,6 +13,7 @@ Window::Window(): View() void Window::Initialize() { + View::Initialize(); /* If we have an application, we try to set our size to something reasonable to begin with - otherwise all windows would default to {0,0} and be invisible by default. */ @@ -68,11 +69,19 @@ void Window::OnDraw() const std::uint32_t titlePos = winMid - titleMid; WriteAt({titlePos, y1}, titleStr); // Contents - for (std::uint32_t y = (y1 + 1); y < y2 ; ++y) + for (std::uint32_t y = y1 + 1; y < y2 ; ++y) { const std::wstring line(x2 - x1 - 1, L' '); WriteAt({x1 + 1, y}, line); } + WriteAt({x1 + 1, y1 + 1}, m_content); + View::OnDraw(); +} + +void Window::OnMouse(const MouseEventData&) +{ + m_content = L"Click!"; + SetDirty(); } std::wstring Window::Title() const diff --git a/mkacct/include/application.h b/mkacct/include/application.h index f46b110..a049faf 100644 --- a/mkacct/include/application.h +++ b/mkacct/include/application.h @@ -13,6 +13,8 @@ public: Application(std::vector args); + virtual ~Application() = default; + }; } // namespace pubnix diff --git a/mkacct/src/mkacct.cpp b/mkacct/src/mkacct.cpp index 79e7c75..c4e6499 100644 --- a/mkacct/src/mkacct.cpp +++ b/mkacct/src/mkacct.cpp @@ -17,11 +17,13 @@ int main(int argc, char* argv[]) { std::vector args(argv + 1, argv + argc); pubnix::Application app(args); - + app.Initialize(); auto mainWindow = std::make_unique(); - mainWindow->Position({10,10}); + mainWindow->Position({10, 10}); + auto otherWindow = std::make_unique(); + otherWindow->Position({20, 20}); app.Add(std::move(mainWindow)); - + app.Add(std::move(otherWindow)); app.Run(); return 0; }