Add mouse handling + other improvements & bugfixes

This commit is contained in:
John Sennesael 2021-08-29 12:55:50 -05:00
parent 50477ab304
commit c5c23d4176
12 changed files with 268 additions and 116 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@
*.sw*
mkacct/build/*
lib/neovision/build/*
lib/neovision/doc/*

View File

@ -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<Application> m_selfPtr;
Terminal m_terminal;
std::vector<std::unique_ptr<View>> 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> 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.

View File

@ -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.

View File

@ -34,11 +34,14 @@ class View
Vector2D m_cursorPosition;
bool m_dirty{true};
bool m_initialized{false};
std::optional<std::reference_wrapper<View>> m_parent{};
Vector2D m_position;
std::mutex m_positionMutex;
ViewBuffer m_previousBuffer;
std::mutex m_previousBufferMutex;
Vector2D m_size;
std::vector<std::unique_ptr<View>> m_views;
std::atomic<std::int32_t> 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> 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<std::reference_wrapper<View>> 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.

View File

@ -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

View File

@ -1,4 +1,5 @@
#include <algorithm>
#include <functional>
#include <unistd.h>
#include "neovision/ansi.h"
@ -25,64 +26,62 @@ Application::~Application()
if (m_running == true) Stop();
}
void Application::Add(std::unique_ptr<View> 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<View>& a, const std::unique_ptr<View>& 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

View File

@ -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} +

View File

@ -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<std::chrono::seconds>(
const auto deltaT = std::chrono::duration_cast<std::chrono::milliseconds>(
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;
}

View File

@ -18,6 +18,24 @@ View::~View()
m_previousBuffer.clear();
}
void View::Add(std::unique_ptr<View> 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<View>& a, const std::unique_ptr<View>& 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<std::reference_wrapper<View>> 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<std::mutex> 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<std::mutex> 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<wchar_t>& 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<wchar_t>& 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

View File

@ -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

View File

@ -13,6 +13,8 @@ public:
Application(std::vector<std::string> args);
virtual ~Application() = default;
};
} // namespace pubnix

View File

@ -17,11 +17,13 @@ int main(int argc, char* argv[])
{
std::vector<std::string> args(argv + 1, argv + argc);
pubnix::Application app(args);
app.Initialize();
auto mainWindow = std::make_unique<neovision::Window>();
mainWindow->Position({10,10});
mainWindow->Position({10, 10});
auto otherWindow = std::make_unique<neovision::Window>();
otherWindow->Position({20, 20});
app.Add(std::move(mainWindow));
app.Add(std::move(otherWindow));
app.Run();
return 0;
}