pubnix/lib/neovision/src/view.cpp

320 lines
8.4 KiB
C++

#include <codecvt>
#include <locale>
#include <string>
#include "neovision/ansi.h"
#include "neovision/application.h"
#include "neovision/crt.h"
#include "neovision/view.h"
namespace neovision {
View::~View()
{
const std::lock_guard<std::mutex> bufferLock(m_bufferMutex);
const std::lock_guard<std::mutex> previousBufferLock(m_previousBufferMutex);
m_buffer.clear();
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;
}
void View::CursorPosition(const Vector2D& pos)
{
m_cursorPosition = pos;
}
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;
}
Vector2D View::Position() const
{
return m_position;
}
void View::Render()
{
DoRender([&](const std::wstring& output, const Vector2D& pos){
if (m_cursorPosition != pos)
{
m_cursorPosition = pos;
SetCursorPos(pos);
}
IO::Get().Write(output);
});
}
void View::Render(View& view)
{
DoRender([&](const std::wstring& output, const Vector2D& p){
view.WriteAt(p, output);
});
}
void View::DoRender(
std::function<void(const std::wstring&, const Vector2D&)> outputFunc
)
{
const std::lock_guard<std::mutex> bufferLock(m_bufferMutex);
/* 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];
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];
const Vector2D pos = m_position + Vector2D(x, y);
if (y < m_previousBuffer.size())
{
const std::vector<wchar_t>& pline = m_previousBuffer[y];
if (x < pline.size())
{
const wchar_t oldChar = pline[x];
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;
}
// Now write output if an update is needed for this character.
if (needUpdate) outputFunc(std::wstring{newChar}, pos);
}
if (!lineOut.empty())
{
const Vector2D pos = m_position + Vector2D(0, y);
outputFunc(lineOut, pos);
}
}
{
const std::lock_guard<std::mutex> previousBufferLock(m_previousBufferMutex);
m_previousBuffer.clear();
for (const auto& l: m_buffer)
{
std::vector<wchar_t> newLine;
for (const wchar_t c: l)
{
newLine.emplace_back(c);
}
m_previousBuffer.emplace_back(newLine);
}
}
m_dirty = false;
}
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)
{
const std::lock_guard<std::mutex> lock(m_bufferMutex);
m_size = s;
const uint32_t sY = s.Y();
const uint32_t sX = s.X();
const uint32_t curRowBufferSize = m_buffer.size();
const uint32_t minRowBufferSize = sY + 1;
const uint32_t minColBufferSize = sX + 1;
if (curRowBufferSize != minRowBufferSize) m_buffer.resize(minRowBufferSize);
for (std::vector<wchar_t>& line: m_buffer)
{
const uint32_t curColBufferSize = line.size();
if (curColBufferSize < minColBufferSize)
{
line.resize(minColBufferSize, fill);
}
else if (curColBufferSize > minColBufferSize)
{
line.resize(minColBufferSize);
}
}
SetDirty(false, false);
}
Vector2D View::Size() const
{
return m_size;
}
void View::Write(const std::wstring& s)
{
if (s.empty() || m_buffer.empty()) return;
WriteToBuffer(m_cursorPosition, s);
}
void View::WriteAt(const Vector2D& p, const std::wstring& s)
{
if (s.empty()) return;
WriteToBuffer(p, s);
}
void View::WriteLn(const std::wstring& s)
{
if (s.empty() || m_buffer.empty()) return;
WriteToBuffer(m_cursorPosition, s + L"\n");
}
void View::WriteToBuffer(const Vector2D& p, const std::wstring s)
{
const std::lock_guard<std::mutex> lock(m_bufferMutex);
if (m_buffer.empty()) return;
if (p.X() > m_size.X()) return;
if (p.Y() > m_size.Y()) return;
// a position-write with a blank string still sets position.
m_cursorPosition.X(p.X());
m_cursorPosition.Y(p.Y());
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
our cursor around. */
for (const wchar_t c: s)
{
if (c == L'\n')
{
if ((m_cursorPosition.Y() + 1) > m_size.Y()) break; // no room.
m_cursorPosition.Y(m_cursorPosition.Y() + 1);
m_cursorPosition.X(0);
}
if (c == 0)
{
m_cursorPosition.X(m_cursorPosition.X() + 1);
continue;
}
/**
* @todo parse all possible ansi escape codes and their effects on the
* cursor's position....yeah,... that'll be fun.
*/
const uint32_t x = m_cursorPosition.X();
const uint32_t y = m_cursorPosition.Y();
if (y > m_buffer.size() - 1) break;
if (x > m_buffer[y].size() - 1) break;
m_buffer[y][x] = c;
m_cursorPosition.X(x + 1);
}
}
std::int32_t View::Zorder() const
{
return m_zOrder;
}
void View::Zorder(std::int32_t z)
{
m_zOrder = z;
const std::shared_ptr<Application> app = TheApp.lock();
if (app == nullptr) return; // No application instantiated yet.
app->CalculateZOrders();
}
} // namespace neovision