Window class: Raise on click + titlebar dragging done

This commit is contained in:
John Sennesael 2021-08-30 19:08:49 -05:00
parent 9a56e6dfaa
commit f4d5c8aefa
8 changed files with 219 additions and 66 deletions

View File

@ -25,6 +25,7 @@ namespace neovision {
class Application: public View
{
wchar_t m_bgFillCharacter{L''};
const std::chrono::milliseconds m_dragDelay{1000};
int m_ttyFd{0};
InputParser m_inputParser;
bool m_running{false};
@ -71,14 +72,17 @@ public:
protected:
virtual void OnDraw();
/**
* @brief Mouse event.
*
* Triggers when a mouse event happens.
*
* @param[in] mouseData Mouse event data gets passed in here.
* @param[in] drag Indicates whether a drag action is taking place.
*/
virtual void OnMouse(const MouseEventData& mouseData);
virtual void OnMouse(const MouseEventData& mouseData, bool drag);
/**
* @brief Key event.

View File

@ -7,6 +7,7 @@
#pragma once
#include <atomic>
#include <chrono>
#include <cstdint>
#include <functional>
#include <iostream>
@ -46,8 +47,8 @@ enum class StandardColor {
*/
class Vector2D {
std::atomic<std::uint32_t> m_x{0};
std::atomic<std::uint32_t> m_y{0};
std::atomic<std::int32_t> m_x{0};
std::atomic<std::int32_t> m_y{0};
public:
@ -59,7 +60,7 @@ public:
/**
* @brief Constructor with unsigned initial values.
*/
Vector2D(std::uint32_t x, std::uint32_t y);
Vector2D(std::int32_t x, std::int32_t y);
/**
* @brief Copy constructor.
@ -78,25 +79,25 @@ public:
* @brief Get X.
* @return Returns the vector x coordinate.
*/
std::uint32_t X() const;
std::int32_t X() const;
/**
* @brief Set X.
* @param n New x value.
*/
void X(std::uint32_t n);
void X(std::int32_t n);
/**
* @brief Get Y.
* @return Returns the vector y coordinate.
*/
std::uint32_t Y() const;
std::int32_t Y() const;
/**
* @brief Set Y.
* @param n New y value.
*/
void Y(std::uint32_t n);
void Y(std::int32_t n);
Vector2D operator+(const Vector2D& other) const;
Vector2D operator+=(const Vector2D& other);
@ -295,25 +296,31 @@ enum class ModKey{ Shift, Ctrl, Meta };
*/
struct MouseEventData {
/**
* @brief Mouse position.
*/
Vector2D position{};
/**
* @brief Mouse button pressed/released.
*/
std::uint8_t m_button;
/**
* @brief Modifier keys pressed.
*/
std::vector<ModKey> m_modifiers;
std::uint8_t button;
/**
* @brief Mouse even type.
*/
ButtonEventType eventType{ButtonEventType::None};
/**
* @brief Modifier keys pressed.
*/
std::vector<ModKey> modifiers;
/**
* @brief Mouse position.
*/
Vector2D position{};
/**
* @brief time of when the event occured.
*/
std::chrono::time_point<std::chrono::system_clock> time{};
};
/**

View File

@ -30,19 +30,16 @@ class View
{
ViewBuffer m_buffer;
std::mutex m_bufferMutex;
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;
std::optional<MouseEventData> m_previousMouseEventData;
Vector2D m_size;
std::vector<std::unique_ptr<View>> m_views;
std::atomic<std::int32_t> m_zOrder{0};
std::int32_t m_zOrder{0};
void DoRender(
std::function<void(const std::wstring&, const Vector2D&)> outputFunc
@ -250,6 +247,21 @@ public:
protected:
/**
* @brief Clear the previous mouse event data.
*/
void ClearPreviousMouseEventData();
/**
* @brief Get previous mouse event data.
*
* This is useful when the mouse is being dragged. Allows access to the
* previous mouse position which could be used to calculate an offset.
*
* @return Returns the previous mouse event data (if any).
*/
std::optional<MouseEventData> GetPreviousMouseEventData() const;
/**
* @brief Initialize event.
*
@ -257,6 +269,14 @@ protected:
*/
virtual void Initialize();
/**
* @brief Bring window to front.
*
* Adjust the z-order such that this window becomes the topmost window,
* relative to the group of windows managed by the parent.
*/
virtual void BringToFront();
/**
* @brief Draw event.
*
@ -280,8 +300,29 @@ protected:
* @brief Mouse event.
*
* Triggers when a mouse event happens.
*
* @param[in] d Mouse event data containing position, button states, etc,...
* @param[in] drag Set to true if the mouse is currently being dragged over
* the view. This happens when a mouse button is pushed down
* and then the mouse position is moved without releasing
* the button. In this case, you can use the
* GetPreviousMouseEventData() function to get the previous
* mouse position.
*/
virtual void OnMouse(const MouseEventData&){};
virtual void OnMouse(const MouseEventData& d, bool drag=false){};
/**
* @brief Set previous mouse event data.
*
* Called by the application onto the view after triggering a mouse event.
* We keep track of the previous event, so we can detect mouse dragging.
*
* Not a function you should have to call as a library user when using a
* normal Application-derrived class for your application.
*
* @param[in] d Mouse event data to store.
*/
void SetPreviousMouseEventData(const MouseEventData& d);
/**
* @brief Write to the internal view buffer.

View File

@ -14,7 +14,9 @@ namespace neovision {
class Window: public View
{
Vector2D m_dragStart;
std::wstring m_title{L"Window"};
bool m_titleDrag{false};
std::wstring m_content;
public:
@ -64,7 +66,7 @@ protected:
*
* Triggers when a mouse event happens.
*/
virtual void OnMouse(const MouseEventData&);
virtual void OnMouse(const MouseEventData& data, bool drag);
};

View File

@ -56,7 +56,7 @@ void Application::Initialize()
OnKey(k);
});
m_inputParser.SetOnMouseEvent([this](const MouseEventData& m){
OnMouse(m);
OnMouse(m, false);
});
InitializeViews();
}
@ -107,7 +107,19 @@ Terminal& Application::Term()
return m_terminal;
}
void Application::OnMouse(const MouseEventData& mouseData)
void Application::OnDraw()
{
// Draw ourselves into the buffer.
std::fill(
m_buffer.begin(),
m_buffer.end(),
std::vector<wchar_t>(m_size.X(), m_bgFillCharacter)
);
// Then draw everything else.
View::OnDraw();
}
void Application::OnMouse(const MouseEventData& mouseData, bool drag)
{
/* 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) */
@ -125,7 +137,26 @@ void Application::OnMouse(const MouseEventData& mouseData)
&& (mouseData.position.Y() >= y1)
&& (mouseData.position.Y() <= y2) )
{
view->OnMouse(mouseData);
// Detect mouse drag.
auto pmd = view->GetPreviousMouseEventData();
if (pmd.has_value())
{
if (pmd->button == mouseData.button)
{
if ( (pmd->eventType == ButtonEventType::Press)
&& (mouseData.eventType == ButtonEventType::Press) )
{
drag = true;
}
}
}
if (drag != true)
{
view->ClearPreviousMouseEventData();
}
// Fire event, set previous data.
view->OnMouse(mouseData, drag);
view->SetPreviousMouseEventData(mouseData);
break;
}
}

View File

@ -1,3 +1,4 @@
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <functional>
@ -21,7 +22,7 @@ Vector2D::Vector2D(): m_x{0}, m_y{0}
{
}
Vector2D::Vector2D(std::uint32_t x, std::uint32_t y): m_x{x}, m_y{y}
Vector2D::Vector2D(std::int32_t x, std::int32_t y): m_x{x}, m_y{y}
{
}
@ -36,29 +37,31 @@ std::string Vector2D::Str() const
return std::to_string(m_x) + "," + std::to_string(m_y);
}
std::uint32_t Vector2D::X() const
std::int32_t Vector2D::X() const
{
return m_x;
}
void Vector2D::X(std::uint32_t n)
void Vector2D::X(std::int32_t n)
{
m_x = n;
}
std::uint32_t Vector2D::Y() const
std::int32_t Vector2D::Y() const
{
return m_y;
}
void Vector2D::Y(std::uint32_t n)
void Vector2D::Y(std::int32_t n)
{
m_y = n;
}
Vector2D Vector2D::operator+(const Vector2D& other) const
{
return Vector2D(m_x + other.m_x, m_y + other.m_y);
const std::int32_t nx = m_x + other.m_x;
const std::int32_t ny = m_y + other.m_y;
return {nx, ny};
}
Vector2D Vector2D::operator+=(const Vector2D& other)
@ -70,7 +73,9 @@ Vector2D Vector2D::operator+=(const Vector2D& other)
Vector2D Vector2D::operator-(const Vector2D& other) const
{
return Vector2D(m_x - other.m_x, m_y - other.m_y);
const std::int32_t nx = m_x - other.m_x;
const std::int32_t ny = m_y - other.m_y;
return {nx, ny};
}
Vector2D Vector2D::operator-=(const Vector2D& other)
@ -249,11 +254,12 @@ void InputParser::Read()
if (extraButtons == true) buttonMultiplier += 4;
if (extraExtraButtons == true) buttonMultiplier += 8;
const std::uint8_t buttons = b & 3;
mouseEvent.m_button = buttons + buttonMultiplier;
mouseEvent.button = buttons + buttonMultiplier;
if (meta == true)
mouseEvent.m_modifiers.emplace_back(ModKey::Meta);
mouseEvent.modifiers.emplace_back(ModKey::Meta);
if (ctrl == true)
mouseEvent.m_modifiers.emplace_back(ModKey::Ctrl);
mouseEvent.modifiers.emplace_back(ModKey::Ctrl);
mouseEvent.time = std::chrono::system_clock::now();
validMouseData = true;
}
}
@ -323,8 +329,8 @@ Vector2D GetCursorPos()
"Could not parse GetCursorPos response from terminal. (2)"
);
}
const std::uint32_t x = std::stoi(responseMatches[1].str());
const std::uint32_t y = std::stoi(responseMatches[2].str());
const std::int32_t x = std::stoi(responseMatches[1].str());
const std::int32_t y = std::stoi(responseMatches[2].str());
return {x, y};
}

View File

@ -12,8 +12,6 @@ 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();
}
@ -27,6 +25,13 @@ void View::Add(std::unique_ptr<View> view)
if (m_initialized == true) viewRef.Initialize();
}
void View::BringToFront()
{
if (!m_parent.has_value()) return;
m_zOrder = m_parent->get().m_views.size();
m_parent->get().CalculateZOrders();
}
void View::CalculateZOrders()
{
std::sort(m_views.begin(), m_views.end(),
@ -34,6 +39,18 @@ void View::CalculateZOrders()
return a->Zorder() < b->Zorder();
}
);
for (size_t nv = 0; nv < m_views.size(); ++nv)
{
if (m_views[nv])
{
m_views[nv]->m_zOrder = nv;
}
}
}
void View::ClearPreviousMouseEventData()
{
m_previousMouseEventData.reset();
}
Vector2D View::CursorPosition() const
@ -53,7 +70,6 @@ bool View::Dirty() const
void View::ForceRedrawAll()
{
const std::lock_guard<std::mutex> previousBufferLock(m_previousBufferMutex);
m_previousBuffer.clear();
}
@ -105,7 +121,6 @@ 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
@ -117,6 +132,7 @@ void View::DoRender(
if (m_parent && (m_parent->get().Dirty() == true)) m_previousBuffer.clear();
for (std::uint32_t y = 0; y != m_size.Y() + 1; ++y)
{
if (y >= m_buffer.size()) break;
std::wstring lineOut;
const std::vector<wchar_t>& line = m_buffer[y];
bool needUpdate{false};
@ -161,7 +177,6 @@ void View::DoRender(
}
}
{
const std::lock_guard<std::mutex> previousBufferLock(m_previousBufferMutex);
m_previousBuffer.clear();
for (const auto& l: m_buffer)
{
@ -176,6 +191,11 @@ void View::DoRender(
m_dirty = false;
}
std::optional<MouseEventData> View::GetPreviousMouseEventData() const
{
return m_previousMouseEventData;
}
void View::OnDraw()
{
// Let all child views draw themselves first.
@ -188,7 +208,6 @@ void View::OnDraw()
v->OnDraw();
}
}
/* Render all child views (they should be sorted by z-order already)
onto ourself. */
for (auto& v: m_views)
@ -221,9 +240,13 @@ void View::SetDirty(bool propagateChildren, bool propagateParent)
}
}
void View::SetPreviousMouseEventData(const MouseEventData& d)
{
m_previousMouseEventData = d;
}
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();
@ -271,7 +294,6 @@ 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;
if (p.X() > m_size.X()) return;
if (p.Y() > m_size.Y()) return;
@ -318,8 +340,7 @@ 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();
if (m_parent.has_value()) m_parent->get().CalculateZOrders();
}
} // namespace neovision

View File

@ -20,36 +20,36 @@ void Window::Initialize()
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;
const std::int32_t sX = appSize.X() / 2;
const std::int32_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()};
const std::int32_t x1{0};
const std::int32_t y1{0};
const std::int32_t x2{s.X()};
const std::int32_t y2{s.Y()};
// Top border
for (std::uint32_t x = (x1 + 1); x < x2; ++x)
for (std::int32_t x = (x1 + 1); x < x2; ++x)
{
WriteAt({x, y1}, L"");
}
// Bottom border
for (std::uint32_t x = (x1 + 1); x < x2; ++x)
for (std::int32_t x = (x1 + 1); x < x2; ++x)
{
WriteAt({x, y2}, L"");
}
// Left border
for (std::uint32_t y = (y1 + 1); y < y2; ++y)
for (std::int32_t y = (y1 + 1); y < y2; ++y)
{
WriteAt({x1, y}, L"");
}
// Right border
for (std::uint32_t y = (y1 + 1); y < y2; ++y)
for (std::int32_t y = (y1 + 1); y < y2; ++y)
{
WriteAt({x2, y}, L"");
}
@ -66,10 +66,10 @@ void Window::OnDraw()
const std::wstring titleStr = L" " + m_title + L" ";
const float winMid = float(s.X()) / 2.0f;
const float titleMid = float(titleStr.length()) / 2.0f;
const std::uint32_t titlePos = winMid - titleMid;
const std::int32_t titlePos = winMid - titleMid;
WriteAt({titlePos, y1}, titleStr);
// Contents
for (std::uint32_t y = y1 + 1; y < y2 ; ++y)
for (std::int32_t y = y1 + 1; y < y2 ; ++y)
{
const std::wstring line(x2 - x1 - 1, L' ');
WriteAt({x1 + 1, y}, line);
@ -78,10 +78,51 @@ void Window::OnDraw()
View::OnDraw();
}
void Window::OnMouse(const MouseEventData&)
void Window::OnMouse(const MouseEventData& d, bool drag)
{
m_content = L"Click!";
SetDirty();
if (d.button != 0) return; // We only handle left-clicks.
// If we receive a click, we raise the window.
BringToFront();
// Titlebar dragging:
const Vector2D s = Size();
const std::int32_t x1{Position().X()};
const std::int32_t y1{Position().Y()};
const std::int32_t x2{x1 + s.X()};
const std::int32_t y2{y1 + s.Y()};
const std::int32_t mx = d.position.X();
const std::int32_t my = d.position.Y();
if (d.eventType == ButtonEventType::Press && m_titleDrag == false)
{
/* If a button is pressed, and we're not already in drag-mode, are we
in the title bar? */
if (my == y1 + 1)
{
// Save the 'start drag position' and toggle titleDrag.
m_dragStart = d.position;
m_titleDrag = true;
}
}
else if (d.eventType == ButtonEventType::Release)
{
// Either way, if the mouse is released, we stop dragging.
m_titleDrag = false;
}
// Once we're actually dragging, move the window.
if ((drag == true) && (m_titleDrag == true))
{
const MouseEventData prev = GetPreviousMouseEventData().value();
const Vector2D offset = d.position - prev.position;
const Vector2D newPos = Position() + offset;
// Avoid dragging off-screen.
const std::int32_t maxX = TheApp.lock()->Size().X() - s.X();
const std::int32_t maxY = TheApp.lock()->Size().Y() - s.Y();
if ( (newPos.Y() >= 0) && (newPos.X() >= 0)
&& (newPos.X() <= maxX)
&& (newPos.Y() <= maxY) )
{
Position(newPos);
}
}
}
std::wstring Window::Title() const