Add window class

This commit is contained in:
John Sennesael 2021-08-20 17:08:55 -05:00
parent f93b5da62d
commit 78a13c1599
12 changed files with 476 additions and 56 deletions

View File

@ -1,5 +1,8 @@
cmake_minimum_required(VERSION 3.5)
set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -ggdb -fno-omit-frame-pointer -fsanitize=address")
set (CMAKE_LINKER_FLAGS_DEBUG "${CMAKE_LINKER_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address")
if(NOT DEFINED CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "DEBUG" CACHE STRING "Type of build: Debug|Release")
endif()
@ -14,6 +17,7 @@ add_library(neovisionObj OBJECT
"src/application.cpp"
"src/crt.cpp"
"src/term.cpp"
"src/window.cpp"
"src/view.cpp"
)

View File

@ -14,6 +14,13 @@ namespace neovision {
* Create a class derrived from this one to serve as your main application
* class.
*
* Unless you're doing something weird, the Application class should be the
* first thing you instantiate.
*
* Add views to the application for anything you want the application to render
* using the Add() function. The application takes ownership of any views added
* to it.
*
*/
class Application: public View
{
@ -22,13 +29,38 @@ class Application: public View
bool m_running{false};
std::shared_ptr<Application> m_selfPtr;
Terminal m_terminal;
std::vector<std::unique_ptr<View>> m_views;
public:
/**
* @brief Default constructor.
*/
Application();
/**
* @brief Destructor.
*/
virtual ~Application();
/**
* @brief Adds a view to the application.
*
* The application will take ownership of the view and render it when
* appropriate etc,...
*
* @param view View to add.
*/
virtual void Add(std::unique_ptr<View> view);
/**
* @brief Starts the application.
*/
virtual void Run();
/**
* @brief Terminates the application.
*/
virtual void Stop();
/**
@ -40,9 +72,34 @@ public:
protected:
/**
* @brief Recalculates the z-order of internal views.
*
* Called automatically by views when their z-order changes, or when new
* views are added.
*/
void CalculateZOrders();
/**
* @brief Initialize event.
*
* Initializes all views when Run() is called. Views added after Run() is
* called get initialized as they get added.
*
*/
virtual void Initialize();
/**
* @brief Application draw event.
*/
virtual void OnDraw();
/**
* @brief Application resize event.
*/
virtual void OnResize(const Vector2D&){};
friend class View;
};
/**

View File

@ -39,8 +39,8 @@ enum class StandardColor {
*/
class Vector2D {
std::int32_t m_x{};
std::int32_t m_y{};
std::uint32_t m_x{};
std::uint32_t m_y{};
public:
@ -49,11 +49,6 @@ public:
*/
Vector2D() = default;
/**
* Constructor with initial values.
*/
Vector2D(std::int32_t x, std::int32_t y);
/**
* Constructor with unsigned initial values.
*/
@ -67,30 +62,32 @@ public:
/**
* @brief Get X.
* @return Returns the position x coordinate.
* @return Returns the vector x coordinate.
*/
std::int32_t X() const;
std::uint32_t X() const;
/**
* @brief Set X.
* @param n New x value.
*/
void X(std::int32_t n);
void X(std::uint32_t n);
/**
* @brief Get Y.
* @return Returns the position y coordinate.
* @return Returns the vector y coordinate.
*/
std::int32_t Y() const;
std::uint32_t Y() const;
/**
* @brief Set Y.
* @param New y value.
*/
void Y(std::int32_t n);
void Y(std::uint32_t n);
Vector2D operator+(const Vector2D& other) const;
Vector2D operator+=(const Vector2D& other);
Vector2D operator-(const Vector2D& other) const;
Vector2D operator-=(const Vector2D& other);
void operator=(const Vector2D& other);
bool operator==(const Vector2D& other) const;
bool operator!=(const Vector2D& other) const;
@ -126,6 +123,11 @@ public:
*/
IO();
/**
* @brief Destructor.
*/
~IO();
/**
* @brief Copy constructor (deleted).
*/

View File

@ -1,5 +1,6 @@
#pragma once
#include <thread>
#include <vector>
#include "neovision/crt.h"
@ -8,6 +9,11 @@ namespace neovision {
class Application;
/**
* @brief Buffer used by views.
*/
typedef std::vector<std::vector<wchar_t>> ViewBuffer;
/**
* @class Represents a renderable view area.
*
@ -22,13 +28,19 @@ class Application;
class View
{
typedef std::vector<std::vector<wchar_t>> ViewBuffer;
ViewBuffer m_buffer;
std::mutex m_bufferMutex;
Vector2D m_cursorPosition;
bool m_dirty{true};
Vector2D m_position;
ViewBuffer m_previousBuffer;
Vector2D m_size;
std::int32_t m_zOrder{0};
void DoRender(
std::function<void(const std::wstring&, const Vector2D&)> outputFunc
);
public:
@ -40,7 +52,7 @@ public:
/**
* @brief Destructor.
*/
virtual ~View() = default;
virtual ~View();
/**
* @brief Get cursor position.
@ -68,6 +80,16 @@ public:
*/
void CursorPosition(const Vector2D& pos);
/**
* @brief Get dirty flag.
*
* Indicates whether the view buffer has been updated since last
* render.
*
* @return Returns true if buffer is dirty.
*/
bool Dirty() const;
/**
* @brief Set position.
*
@ -83,12 +105,28 @@ public:
Vector2D Position() const;
/**
* @brief Render buffer.
* @brief Render buffer to IO.
*
* Writes the buffer to the current IO stream.
*/
void Render();
/**
* @brief Render buffer to another view.
*
* Writes the buffer to the given view.
*
* @param[out] view View to render to.
*/
void Render(View& view);
/**
* @brief Set dirty flag true.
*
* Forces the view to render next render event.
*/
void SetDirty();
/**
* @brief Set size.
*
@ -96,7 +134,7 @@ public:
* @param fill Fill character for newly created blank space (if growing).
* (optional - defaults to spaces.)
*/
void Size(const Vector2D& s, wchar_t fill = L' ');
void Size(const Vector2D& s, wchar_t fill = 0);
/**
* @brief Get size.
@ -137,8 +175,53 @@ public:
*/
void WriteLn(const std::wstring& s);
/**
* @brief Get the view z-order.
*
* The z order indicates the order in which views are rendered.
* Views with a higher z-order are drawn last, which means they appear on
* top of windows with a lower z-order. As such it can also be thought of as
* the z axis position of a window, where positive points towards the viewer
* and negative points away from the viewer.
*
* @return Returns the view's z-order.
*/
std::int32_t Zorder() const;
/**
* @brief Set the view z-order.
*
* The z order indicates the order in which views are rendered.
* Views with a higher z-order are drawn last, which means they appear on
* top of windows with a lower z-order. As such it can also be thought of as
* the z axis position of a window, where positive points towards the viewer
* and negative points away from the viewer.
*
* @param z The new z-order.
*/
void Zorder(std::int32_t z);
protected:
/**
* @brief Initialize event.
*
* This runs when the application initializes the view.
*/
virtual void Initialize() = 0;
/**
* @brief Draw event.
*
* This runs when the application wants the view to draw itself.
* This is where you'd draw stuff into the view's buffer.
* (As opposed to Render(), which renders the buffer to a desination like
* the screen or another view).
*
* Must be implemented when creating a view.
*/
virtual void OnDraw() = 0;
/**
* @brief Write to the internal view buffer.
*
@ -150,6 +233,8 @@ protected:
*/
virtual void WriteToBuffer(const Vector2D& p, const std::wstring s);
friend class Application;
};
} // namespace neovision

View File

@ -0,0 +1,46 @@
#pragma once
#include "neovision/view.h"
namespace neovision {
/**
* @brief A GUI window.
*
* Windows are Views with a frame around, optionally with a title bar, and all
* sorts of other features,...
*
*/
class Window: public View
{
public:
/**
* @brief Default constructor.
*/
Window();
/**
* @brief Destructor.
*/
virtual ~Window() = default;
protected:
/**
* @brief Initialize event.
*
* This runs when the application initializes the view.
*/
virtual void Initialize();
/**
* @brief Draw event.
*
* Draws the window into the view buffer.
*/
virtual void OnDraw();
};
} // namespace neovision

View File

@ -1,3 +1,4 @@
#include <algorithm>
#include <unistd.h>
#include "neovision/ansi.h"
@ -24,6 +25,31 @@ Application::~Application()
if (m_running) Stop();
}
void Application::Add(std::unique_ptr<View> view)
{
View& viewRef = *view;
m_views.emplace_back(std::move(view));
CalculateZOrders();
if (m_running) 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()
{
for (auto& view: m_views)
{
view->Initialize();
}
}
void Application::Run()
{
/*
@ -32,16 +58,21 @@ void Application::Run()
) + ansi::MakeDecPmSequence(ansi::DecPmSequence::DECTECM,
ansi::DecPmSuffix::DISABLE
));
*/
*/
ClearScreen();
m_terminal.Unbuffer(STDIN_FILENO);
m_running = true;
Size(m_terminal.Size(STDIN_FILENO), m_bgFillCharacter);
m_terminal.SetOnResize([&](Application&){
const Vector2D newSize = m_terminal.Size(STDIN_FILENO);
Size(newSize, m_bgFillCharacter);
OnResize(newSize);
m_terminal.Unbuffer(STDIN_FILENO);
Size(m_terminal.Size(STDIN_FILENO) - Vector2D(1, 1), m_bgFillCharacter);
m_terminal.SetOnResize([](Application& app){
const Vector2D newSize = app.m_terminal.Size(STDIN_FILENO) - Vector2D(1, 1);
app.Size(newSize, app.m_bgFillCharacter);
for (const auto& v: app.m_views)
{
v->SetDirty();
}
app.OnResize(newSize);
});
Initialize();
while (m_running)
{
m_terminal.ProcessEvents(STDIN_FILENO);
@ -69,8 +100,26 @@ Terminal& Application::Term()
void Application::OnDraw()
{
// Let all views render themselves first.
for (auto& v: m_views)
{
if (v->Dirty() == true)
{
v->OnDraw();
}
}
// 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();
sleep(1);
}
} // namespace neovision

View File

@ -16,37 +16,31 @@ namespace neovision {
// Position struct -------------------------------------------------------------
Vector2D::Vector2D(std::int32_t x, std::int32_t y): m_x{x}, m_y{y}
Vector2D::Vector2D(std::uint32_t x, std::uint32_t y): m_x{x}, m_y{y}
{
}
Vector2D::Vector2D(std::uint32_t x, std::uint32_t y)
{
m_x = x;
m_y = y;
}
std::string Vector2D::Str() const
{
return std::to_string(m_x) + "," + std::to_string(m_y);
}
std::int32_t Vector2D::X() const
std::uint32_t Vector2D::X() const
{
return m_x;
}
void Vector2D::X(std::int32_t n)
void Vector2D::X(std::uint32_t n)
{
m_x = n;
}
std::int32_t Vector2D::Y() const
std::uint32_t Vector2D::Y() const
{
return m_y;
}
void Vector2D::Y(std::int32_t n)
void Vector2D::Y(std::uint32_t n)
{
m_y = n;
}
@ -63,6 +57,18 @@ Vector2D Vector2D::operator+=(const Vector2D& other)
return *this;
}
Vector2D Vector2D::operator-(const Vector2D& other) const
{
return Vector2D(m_x - other.m_x, m_y - other.m_y);
}
Vector2D Vector2D::operator-=(const Vector2D& other)
{
m_x -= other.m_x;
m_y -= other.m_y;
return *this;
}
void Vector2D::operator=(const Vector2D& other)
{
m_x = other.m_x;
@ -91,6 +97,11 @@ IO::IO():
std::wcout.imbue(std::locale());
}
IO::~IO()
{
std::locale::global(std::locale("C"));
}
IO& IO::Get()
{
static IO instance;
@ -298,6 +309,8 @@ std::wstring ToAnsiBgColor(StandardColor c)
return ansi::SgrSequence::BG_BRIGHT_MAGENTA;
case StandardColor::BRIGHT_WHITE:
return ansi::SgrSequence::BG_BRIGHT_WHITE;
default:
return ansi::SgrSequence::BG_BLACK;
}
}
@ -337,6 +350,8 @@ std::wstring ToAnsiFgColor(StandardColor c)
return ansi::SgrSequence::FG_BRIGHT_MAGENTA;
case StandardColor::BRIGHT_WHITE:
return ansi::SgrSequence::FG_BRIGHT_WHITE;
default:
return ansi::SgrSequence::FG_BLACK;
}
}

View File

@ -31,6 +31,7 @@ Terminal::~Terminal()
{
Restore(setting.first);
}
m_savedTerminalSettings.clear();
}
void Terminal::sigWinchHandler(int)

View File

@ -10,6 +10,13 @@
namespace neovision {
View::~View()
{
const std::lock_guard<std::mutex> lock(m_bufferMutex);
m_buffer.clear();
m_previousBuffer.clear();
}
Vector2D View::CursorPosition() const
{
return m_cursorPosition;
@ -20,6 +27,11 @@ void View::CursorPosition(const Vector2D& pos)
m_cursorPosition = pos;
}
bool View::Dirty() const
{
return m_dirty;
}
void View::Position(const Vector2D& p)
{
m_position = p;
@ -33,13 +45,33 @@ Vector2D View::Position() const
void View::Render()
{
SetCursorPos(m_position);
for (std::uint32_t y = 0; y != m_buffer.size(); ++y)
DoRender([](const std::wstring& output, const Vector2D&){
IO::Get().Write(output);
});
}
void View::Render(View& view)
{
m_previousBuffer.clear();
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> lock(m_bufferMutex);
if (!m_dirty) return;
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};
for (std::uint32_t x = 0; x < line.size(); ++x)
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())
@ -67,9 +99,8 @@ void View::Render()
}
else
{
const Vector2D newPos = m_position + Vector2D(x, y);
SetCursorPos(newPos);
IO::Get().Write(lineOut);
const Vector2D pos = m_position + Vector2D(x, y);
outputFunc(lineOut, pos);
lineOut.clear();
}
lastUpdatedX = x;
@ -77,20 +108,51 @@ void View::Render()
}
if (!lineOut.empty())
{
IO::Get().Write(lineOut);
const Vector2D pos = m_position + Vector2D(0, y);
outputFunc(lineOut, pos);
}
}
m_previousBuffer = m_buffer;
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::SetDirty()
{
m_dirty = true;
}
void View::Size(const Vector2D& s, wchar_t fill)
{
const std::lock_guard<std::mutex> lock(m_bufferMutex);
m_size = s;
m_buffer.resize(s.Y());
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)
{
line.resize(s.X(), fill);
const uint32_t curColBufferSize = line.size();
if (curColBufferSize < minColBufferSize)
{
line.resize(minColBufferSize, fill);
}
else if (curColBufferSize > minColBufferSize)
{
line.resize(minColBufferSize);
}
}
m_dirty = true;
}
Vector2D View::Size() const
@ -118,12 +180,15 @@ void View::WriteLn(const std::wstring& s)
void View::WriteToBuffer(const Vector2D& p, const std::wstring s)
{
const std::lock_guard<std::mutex> lock(m_bufferMutex);
if (m_buffer.empty()) return;
// First set cursor position, bounded by our size.
m_cursorPosition.X( p.X() > m_size.X() ? m_size.X() : p.X() );
m_cursorPosition.Y( p.Y() > m_size.Y() ? m_size.Y() : p.Y() );
if (p.X() > m_size.X()) return;
if (p.Y() > m_size.Y()) return;
// a position-write with a blank string still sets position.
if (s.empty()) return;
m_cursorPosition.X(p.X());
m_cursorPosition.Y(p.Y());
m_dirty = true;
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. */
@ -135,14 +200,35 @@ void View::WriteToBuffer(const Vector2D& p, const std::wstring s)
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.
*/
if ((m_cursorPosition.X() + 1) > m_size.X()) break; // no room.
m_cursorPosition.X(m_cursorPosition.X() + 1);
m_buffer[m_cursorPosition.Y()][m_cursorPosition.X()] = c;
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

View File

@ -0,0 +1,68 @@
#include <cstdint>
#include <memory>
#include "neovision/application.h"
#include "neovision/window.h"
namespace neovision {
Window::Window(): View()
{
}
void Window::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. */
const std::shared_ptr<Application> app = TheApp.lock();
if (app == nullptr) return; // No application instantiated yet.
const Vector2D appSize = app->Size();
const std::uint32_t sX = appSize.X() / 2;
const std::uint32_t sY = appSize.Y() / 2;
this->Size({sX, sY});
}
void Window::OnDraw()
{
const Vector2D s = Size();
const std::uint32_t x1{0};
const std::uint32_t y1{0};
const std::uint32_t x2{s.X()};
const std::uint32_t y2{s.Y()};
// Top border
for (std::uint32_t x = (x1 + 1); x < x2; ++x)
{
WriteAt({x, y1}, L"");
}
// Bottom border
for (std::uint32_t x = (x1 + 1); x < x2; ++x)
{
WriteAt({x, y2}, L"");
}
// Left border
for (std::uint32_t y = (y1 + 1); y < y2; ++y)
{
WriteAt({x1, y}, L"");
}
// Right border
for (std::uint32_t y = (y1 + 1); y < y2; ++y)
{
WriteAt({x2, y}, L"");
}
// Corners
WriteAt({x1, y1}, L"");
WriteAt({x2, y1}, L"");
WriteAt({x1, y2}, L"");
WriteAt({x2, y2}, L"");
// Contents
for (std::uint32_t y = (y1 + 1); y < y2 ; ++y)
{
const std::wstring line(x2 - x1 - 1, L' ');
WriteAt({x1 + 1, y}, line);
}
}
} // namespace neovision

View File

@ -1,5 +1,8 @@
cmake_minimum_required(VERSION 3.5)
set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -ggdb -fno-omit-frame-pointer -fsanitize=address")
set (CMAKE_LINKER_FLAGS_DEBUG "${CMAKE_LINKER_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address")
if(NOT DEFINED CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "DEBUG" CACHE STRING "Type of build: Debug|Release")
endif()
@ -24,5 +27,5 @@ target_include_directories(mkacct
PRIVATE
include
PUBLIC
${NEOVISION_INCLUDE_DIRECTORIES}
${NEOVISION_INCLUDE_DIRS}
)

View File

@ -5,8 +5,7 @@
#include <string>
#include <iostream>
#include "neovision/crt.h"
#include "neovision/term.h"
#include "neovision/window.h"
#include "application.h"
@ -18,6 +17,11 @@ int main(int argc, char* argv[])
{
std::vector<std::string> args(argv + 1, argv + argc);
pubnix::Application app(args);
auto mainWindow = std::make_unique<neovision::Window>();
mainWindow->Position({3,5});
app.Add(std::move(mainWindow));
app.Run();
return 0;
}