Add correct fonts
This commit is contained in:
@@ -13,6 +13,7 @@ target_link_libraries(DisplayGraphics
|
||||
Helpers::All
|
||||
Display::UI::Text
|
||||
Display::UI::HostBlock
|
||||
Display::UI::Header
|
||||
)
|
||||
|
||||
target_include_directories(DisplayGraphics
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace Display::Graphics
|
||||
{
|
||||
framebuffer.clear(Color{0, 0, 0});
|
||||
|
||||
header.draw(framebuffer, textRenderer, START_X, START_Y);
|
||||
|
||||
int blocksPerRow =
|
||||
(SCREEN_WIDTH + BLOCK_GAP) / (Display::UI::HostBlock::BLOCK_WIDTH + BLOCK_GAP);
|
||||
if (blocksPerRow < 1)
|
||||
@@ -25,7 +27,7 @@ namespace Display::Graphics
|
||||
int row = index / blocksPerRow;
|
||||
|
||||
int x = START_X + col * (Display::UI::HostBlock::BLOCK_WIDTH + BLOCK_GAP);
|
||||
int y = START_Y + row * (Display::UI::HostBlock::BLOCK_HEIGHT + BLOCK_GAP);
|
||||
int y = START_Y + header.height() + BLOCK_GAP + row * (Display::UI::HostBlock::BLOCK_HEIGHT + BLOCK_GAP);
|
||||
|
||||
hostblock.draw(
|
||||
framebuffer,
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
#include "Display/Graphics/Framebuffer.h"
|
||||
#include "Model/HostRegistry.h"
|
||||
#include "Display/UI/Text/Renderer.h"
|
||||
#include "Display/UI/Header/Header.h"
|
||||
#include "Display/UI/HostBlock/HostBlock.h"
|
||||
#include "Display/Graphics/Color.h"
|
||||
#include <string>
|
||||
|
||||
namespace Display::Graphics
|
||||
{
|
||||
static constexpr int START_X = 0;
|
||||
static constexpr int START_Y = 10;
|
||||
static constexpr int BLOCK_GAP = 4;
|
||||
static constexpr int START_X = 0; // 0
|
||||
static constexpr int START_Y = 0; // 20
|
||||
static constexpr int BLOCK_GAP = 4; // 4
|
||||
|
||||
static constexpr int SCREEN_WIDTH = 240;
|
||||
|
||||
@@ -28,6 +29,7 @@ namespace Display::Graphics
|
||||
Framebuffer &framebuffer;
|
||||
Model::HostRegistry ®istry;
|
||||
Display::UI::Text::Renderer textRenderer;
|
||||
Display::UI::Header::Header header;
|
||||
Display::UI::HostBlock::HostBlock hostblock;
|
||||
};
|
||||
} // namespace Display::Graphics
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Display/Graphics/Color.h"
|
||||
#include "Display/UI/Theme/Theme.h"
|
||||
|
||||
namespace Display::UI::Bar
|
||||
{
|
||||
@@ -12,9 +13,9 @@ namespace Display::UI::Bar
|
||||
bool drawBorder;
|
||||
|
||||
Style(
|
||||
Display::Graphics::Color background = Display::Graphics::Color{30, 30, 30},
|
||||
Display::Graphics::Color fill = Display::Graphics::Color{0, 180, 0},
|
||||
Display::Graphics::Color border = Display::Graphics::Color{80, 80, 80},
|
||||
Display::Graphics::Color background = Display::UI::Theme::Bar::BACKGROUND,
|
||||
Display::Graphics::Color fill = Display::UI::Theme::Bar::FILL,
|
||||
Display::Graphics::Color border = Display::UI::Theme::Bar::BORDER,
|
||||
bool drawBorder = true)
|
||||
: background(background),
|
||||
fill(fill),
|
||||
|
||||
@@ -2,4 +2,6 @@ message(STATUS "··Configuring UI")
|
||||
|
||||
add_subdirectory(Bar)
|
||||
add_subdirectory(Text)
|
||||
add_subdirectory(HostBlock)
|
||||
add_subdirectory(Header)
|
||||
add_subdirectory(HostBlock)
|
||||
add_subdirectory(Theme)
|
||||
@@ -0,0 +1,18 @@
|
||||
message(STATUS "···Configuring Header")
|
||||
|
||||
add_library(DisplayUIHeader
|
||||
Header.cpp
|
||||
)
|
||||
|
||||
add_library(Display::UI::Header ALIAS DisplayUIHeader)
|
||||
|
||||
target_link_libraries(DisplayUIHeader
|
||||
PUBLIC
|
||||
Display::Graphics
|
||||
Display::UI::Text
|
||||
)
|
||||
|
||||
target_include_directories(DisplayUIHeader
|
||||
PUBLIC
|
||||
${INCLUDE_BASE_DIR}
|
||||
)
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "Display/UI/Header/Header.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
#include "Display/UI/Bar/Orientation.h"
|
||||
#include "Display/UI/Bar/Style.h"
|
||||
#include "Display/UI/Theme/Theme.h"
|
||||
#include "Display/Graphics/Color.h"
|
||||
|
||||
namespace Display::UI::Header
|
||||
{
|
||||
Header::Header()
|
||||
{
|
||||
}
|
||||
|
||||
void Header::draw(
|
||||
Graphics::Framebuffer &fb,
|
||||
UI::Text::Renderer &text,
|
||||
int x, int y)
|
||||
{
|
||||
// ===== Block background =====
|
||||
fb.fillRect(x, y, HEADER_WIDTH, HEADER_HEIGHT, Display::UI::Theme::Header::BACKGROUND);
|
||||
fb.drawRect(x, y, HEADER_WIDTH, HEADER_HEIGHT, Display::UI::Theme::Header::BORDER);
|
||||
|
||||
int cursorY = y + PADDING;
|
||||
|
||||
text.drawTextOutlined(
|
||||
fb,
|
||||
x + PADDING + 120 - 2,
|
||||
cursorY + 8,
|
||||
getCurrentDateTime(),
|
||||
Display::UI::Theme::Text::TEXT,
|
||||
Display::UI::Theme::Text::OUTLINE,
|
||||
Display::UI::Text::Font{std::string(HEADER_FONT_NAME.begin(), HEADER_FONT_NAME.end()), HEADER_FONT_SIZE});
|
||||
}
|
||||
|
||||
std::string Header::getCurrentDateTime()
|
||||
{
|
||||
auto now = std::chrono::system_clock::now();
|
||||
std::time_t t = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm tm{};
|
||||
localtime_r(&t, &tm);
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << std::setfill('0')
|
||||
<< std::setw(2) << tm.tm_mday << '/'
|
||||
<< std::setw(2) << tm.tm_mon + 1 << '/'
|
||||
<< tm.tm_year + 1900 << ' '
|
||||
<< std::setw(2) << tm.tm_hour << ':'
|
||||
<< std::setw(2) << tm.tm_min << ':'
|
||||
<< std::setw(2) << tm.tm_sec;
|
||||
return oss.str();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Display/Graphics/Framebuffer.h"
|
||||
#include "Display/Graphics/Color.h"
|
||||
#include "Display/UI/Text/Renderer.h"
|
||||
|
||||
namespace Display::UI::Header
|
||||
{
|
||||
// ===== Layout =====
|
||||
constexpr int HEADER_WIDTH = 240; // 116
|
||||
constexpr int HEADER_HEIGHT = 16; // 116
|
||||
constexpr int PADDING = 4; // 4
|
||||
constexpr std::string_view HEADER_FONT_NAME = "Pixel10";
|
||||
constexpr int HEADER_FONT_SIZE = 14; // 12
|
||||
|
||||
class Header
|
||||
{
|
||||
public:
|
||||
Header();
|
||||
|
||||
static constexpr int width() { return HEADER_WIDTH; }
|
||||
static constexpr int height() { return HEADER_HEIGHT; }
|
||||
|
||||
void draw(
|
||||
Graphics::Framebuffer &fb,
|
||||
UI::Text::Renderer &text,
|
||||
int x, int y);
|
||||
|
||||
private:
|
||||
std::string getCurrentDateTime();
|
||||
};
|
||||
|
||||
}
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
#include "Display/UI/Bar/Orientation.h"
|
||||
#include "Display/UI/Bar/Style.h"
|
||||
#include "Display/UI/Theme/Theme.h"
|
||||
#include "Display/Graphics/Color.h"
|
||||
#include "Display/UI/Text/Helpers.h"
|
||||
|
||||
namespace Display::UI::HostBlock
|
||||
{
|
||||
@@ -19,6 +21,7 @@ namespace Display::UI::HostBlock
|
||||
MEM_BAR_HEIGHT,
|
||||
Display::UI::Bar::Orientation::Horizontal,
|
||||
Display::UI::Bar::Style{Display::Graphics::Color{40, 40, 40}, Display::Graphics::Color{0, 120, 200}, Display::Graphics::Color{80, 80, 80}, true})
|
||||
|
||||
{
|
||||
}
|
||||
|
||||
@@ -30,8 +33,8 @@ namespace Display::UI::HostBlock
|
||||
const Metrics::Host &metrics)
|
||||
{
|
||||
// ===== Block background =====
|
||||
fb.fillRect(x, y, BLOCK_WIDTH, BLOCK_HEIGHT, BG_COLOR);
|
||||
fb.drawRect(x, y, BLOCK_WIDTH, BLOCK_HEIGHT, BORDER_COLOR);
|
||||
fb.fillRect(x, y, BLOCK_WIDTH, BLOCK_HEIGHT, Display::UI::Theme::HostBlock::BACKGROUND);
|
||||
fb.drawRect(x, y, BLOCK_WIDTH, BLOCK_HEIGHT, Display::UI::Theme::HostBlock::BORDER);
|
||||
|
||||
int cursorY = y + PADDING;
|
||||
|
||||
@@ -41,14 +44,18 @@ namespace Display::UI::HostBlock
|
||||
cursorY,
|
||||
BLOCK_WIDTH - PADDING * 2,
|
||||
HEADER_HEIGHT,
|
||||
HEADER_BG);
|
||||
Display::UI::Theme::HostBlock::HEADER);
|
||||
|
||||
text.drawText(
|
||||
text.drawTextOutlined(
|
||||
fb,
|
||||
x + PADDING + 2,
|
||||
cursorY + HEADER_HEIGHT - 3,
|
||||
hostname,
|
||||
TEXT_COLOR);
|
||||
Display::UI::Theme::Text::TEXT,
|
||||
Display::UI::Theme::Text::OUTLINE,
|
||||
// Display::UI::Text::Font{"LiberationSans-Regular", HEADER_FONT_SIZE});
|
||||
// Display::UI::Text::Font{"PixelFive-Regular", 5});
|
||||
Display::UI::Text::Font{std::string(HEADER_FONT_NAME.begin(), HEADER_FONT_NAME.end()), HEADER_FONT_SIZE});
|
||||
|
||||
cursorY += HEADER_HEIGHT + SECTION_GAP;
|
||||
|
||||
@@ -77,17 +84,63 @@ namespace Display::UI::HostBlock
|
||||
SECTION_GAP;
|
||||
|
||||
// ===== Memory bar =====
|
||||
float memValue = 0.0f;
|
||||
if (metrics.memory.total > 0)
|
||||
{
|
||||
memValue = float(metrics.memory.used) /
|
||||
float(metrics.memory.total);
|
||||
}
|
||||
float memValue, swapValue = 0.0f;
|
||||
if (metrics.memory.mem_total > 0)
|
||||
memValue = metrics.memory.mem_used / metrics.memory.mem_total;
|
||||
|
||||
memBar.draw(
|
||||
fb,
|
||||
x + PADDING,
|
||||
cursorY,
|
||||
std::clamp(memValue, 0.0f, 1.0f));
|
||||
text.drawTextOutlined(fb,
|
||||
x + MEM_BAR_TEXT_PADDING_X,
|
||||
cursorY + 8,
|
||||
"M: " + Display::UI::Text::formatFloat(metrics.memory.mem_used / 1073741824) + "/" + Display::UI::Text::formatFloat(metrics.memory.mem_total / 1073741824),
|
||||
Display::UI::Theme::Text::TEXT,
|
||||
Display::UI::Theme::Text::OUTLINE,
|
||||
Display::UI::Text::Font{"PixelFive-Regular", 5});
|
||||
|
||||
cursorY += MEM_BAR_HEIGHT + SECTION_GAP;
|
||||
|
||||
if (metrics.memory.swap_total > 0)
|
||||
{
|
||||
swapValue = metrics.memory.swap_used / metrics.memory.swap_total;
|
||||
|
||||
memBar.draw(
|
||||
fb,
|
||||
x + PADDING,
|
||||
cursorY,
|
||||
std::clamp(swapValue, 0.0f, 1.0f));
|
||||
text.drawTextOutlined(fb,
|
||||
x + MEM_BAR_TEXT_PADDING_X,
|
||||
cursorY + 8,
|
||||
"S: " + Display::UI::Text::formatFloat(metrics.memory.swap_used / 1073741824) + "/" + Display::UI::Text::formatFloat(metrics.memory.swap_total / 1073741824),
|
||||
Display::UI::Theme::Text::TEXT,
|
||||
Display::UI::Theme::Text::OUTLINE,
|
||||
Display::UI::Text::Font{"PixelFive-Regular", 5});
|
||||
|
||||
cursorY += MEM_BAR_HEIGHT + SECTION_GAP;
|
||||
}
|
||||
|
||||
// ===== Disk bar =====
|
||||
int diskCount = metrics.disks.size();
|
||||
|
||||
for (int i = 0; i < diskCount; ++i)
|
||||
{
|
||||
int by = cursorY +
|
||||
i * (MEM_BAR_HEIGHT + SECTION_GAP);
|
||||
|
||||
float value = std::clamp(metrics.disks[i].used / metrics.disks[i].total, 0.0f, 1.0f);
|
||||
|
||||
memBar.draw(fb, x + PADDING, by, value);
|
||||
text.drawTextOutlined(fb,
|
||||
x + MEM_BAR_TEXT_PADDING_X,
|
||||
by + 8,
|
||||
metrics.disks[i].name + ": " + Display::UI::Text::formatFloat(metrics.disks[i].used / 1073741824) + "/" + Display::UI::Text::formatFloat(metrics.disks[i].total / 1073741824),
|
||||
Display::UI::Theme::Text::TEXT,
|
||||
Display::UI::Theme::Text::OUTLINE,
|
||||
Display::UI::Text::Font{"PixelFive-Regular", 5});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,37 +12,33 @@
|
||||
namespace Display::UI::HostBlock
|
||||
{
|
||||
// ===== Layout =====
|
||||
constexpr int BLOCK_WIDTH = 116;
|
||||
constexpr int BLOCK_HEIGHT = 116;
|
||||
constexpr int PADDING = 4;
|
||||
constexpr int BLOCK_WIDTH = 118; // 118
|
||||
constexpr int BLOCK_HEIGHT = 146; // 146
|
||||
constexpr int PADDING = 5; // 5
|
||||
|
||||
constexpr int HEADER_HEIGHT = 16;
|
||||
constexpr int SECTION_GAP = 4;
|
||||
constexpr int HEADER_HEIGHT = 12; // 12
|
||||
constexpr std::string_view HEADER_FONT_NAME = "PixelFive-Regular";
|
||||
constexpr int HEADER_FONT_SIZE = 5; // 10
|
||||
constexpr int SECTION_GAP = 4; // 4
|
||||
|
||||
// ===== CPU bars =====
|
||||
constexpr int CPU_BAR_WIDTH = 10;
|
||||
constexpr int CPU_BAR_GAP = 4;
|
||||
constexpr int CPU_BAR_HEIGHT = 36;
|
||||
// constexpr int CPU_BAR_HEIGHT = 10;
|
||||
constexpr int CPU_MAX_PER_ROW = 8;
|
||||
constexpr int CPU_ROWS = 2;
|
||||
constexpr int CPU_BAR_WIDTH = 10; // 10
|
||||
constexpr int CPU_BAR_GAP = 4; // 4
|
||||
constexpr int CPU_BAR_HEIGHT = 16; // 16
|
||||
constexpr int CPU_MAX_PER_ROW = 8; // 8
|
||||
constexpr int CPU_ROWS = 2; // 2
|
||||
|
||||
// ===== Memory bar =====
|
||||
constexpr int MEM_BAR_HEIGHT = 8;
|
||||
|
||||
// ===== Colors =====
|
||||
const Display::Graphics::Color BG_COLOR{20, 20, 20};
|
||||
const Display::Graphics::Color BORDER_COLOR{60, 60, 60};
|
||||
const Display::Graphics::Color TEXT_COLOR{220, 220, 220};
|
||||
const Display::Graphics::Color HEADER_BG{30, 30, 30};
|
||||
constexpr int MEM_BAR_HEIGHT = 11; // 8
|
||||
constexpr int MEM_BAR_TEXT_PADDING_X = PADDING + 3;
|
||||
|
||||
class HostBlock
|
||||
{
|
||||
public:
|
||||
HostBlock();
|
||||
|
||||
static constexpr int width() { return 116; }
|
||||
static constexpr int height() { return 116; }
|
||||
static constexpr int width() { return BLOCK_WIDTH; }
|
||||
static constexpr int height() { return BLOCK_HEIGHT; }
|
||||
|
||||
void draw(
|
||||
Graphics::Framebuffer &fb,
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
#include "Display/UI/Text/BitmapFont.h"
|
||||
|
||||
namespace Display::UI::Text
|
||||
{
|
||||
// 5x7 bitmap digits (0–9)
|
||||
static const uint8_t digits[10][7] = {
|
||||
{0x1E, 0x29, 0x25, 0x23, 0x21, 0x29, 0x1E}, // 0
|
||||
{0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E}, // 1
|
||||
{0x1E, 0x21, 0x01, 0x0E, 0x10, 0x20, 0x3F}, // 2
|
||||
{0x1E, 0x21, 0x02, 0x06, 0x01, 0x21, 0x1E}, // 3
|
||||
{0x02, 0x06, 0x0A, 0x12, 0x3F, 0x02, 0x02}, // 4
|
||||
{0x3F, 0x20, 0x3E, 0x01, 0x01, 0x21, 0x1E}, // 5
|
||||
{0x0E, 0x10, 0x20, 0x3E, 0x21, 0x21, 0x1E}, // 6
|
||||
{0x3F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08}, // 7
|
||||
{0x1E, 0x21, 0x21, 0x1E, 0x21, 0x21, 0x1E}, // 8
|
||||
{0x1E, 0x21, 0x21, 0x1F, 0x01, 0x02, 0x1C} // 9
|
||||
};
|
||||
|
||||
void BitmapFont::drawDigit(Display::Graphics::Framebuffer &fb, int x, int y, int d, const Display::Graphics::Color &color)
|
||||
{
|
||||
if (d < 0 || d > 9)
|
||||
return;
|
||||
|
||||
for (int row = 0; row < 7; ++row)
|
||||
{
|
||||
for (int col = 0; col < 5; ++col)
|
||||
{
|
||||
if (digits[d][row] & (1 << (4 - col)))
|
||||
{
|
||||
// Рисуем пиксель как 2x2 квадрат для читаемости
|
||||
fb.fillRect(x + col * 2, y + row * 2, 2, 2, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace Display::UI::Text
|
||||
@@ -1,13 +0,0 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include "Display/Graphics/Framebuffer.h"
|
||||
#include "Display/Graphics/Color.h"
|
||||
|
||||
namespace Display::UI::Text
|
||||
{
|
||||
class BitmapFont
|
||||
{
|
||||
public:
|
||||
void drawDigit(Display::Graphics::Framebuffer &fb, int x, int y, int d, const Display::Graphics::Color &color);
|
||||
};
|
||||
} // namespace Display::UI::Text
|
||||
@@ -1,9 +1,8 @@
|
||||
message(STATUS "···Configuring Text")
|
||||
|
||||
add_library(DisplayUIText
|
||||
BitmapFont.cpp
|
||||
FontFace.cpp
|
||||
GlyphCache.cpp
|
||||
Fonts.cpp
|
||||
Helpers.cpp
|
||||
Renderer.cpp
|
||||
)
|
||||
|
||||
@@ -15,6 +14,7 @@ find_package(Freetype REQUIRED)
|
||||
target_link_libraries(DisplayUIText
|
||||
PRIVATE
|
||||
Display::Graphics
|
||||
Helpers::All
|
||||
${FREETYPE_LIBRARIES}
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Display::UI::Text
|
||||
{
|
||||
struct Font
|
||||
{
|
||||
std::string name;
|
||||
int size; // размер шрифта
|
||||
bool operator==(const Font &other) const
|
||||
{
|
||||
return name == other.name && size == other.size;
|
||||
}
|
||||
};
|
||||
|
||||
struct FontHash
|
||||
{
|
||||
std::size_t operator()(const Font &k) const
|
||||
{
|
||||
return std::hash<std::string>()(k.name) ^ std::hash<int>()(k.size);
|
||||
}
|
||||
};
|
||||
} // namespace Display::UI::Text
|
||||
@@ -1,38 +0,0 @@
|
||||
#include "Display/UI/Text/FontFace.h"
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
#include <stdexcept>
|
||||
|
||||
namespace Display::UI::Text
|
||||
{
|
||||
struct FontFace::FreeType
|
||||
{
|
||||
FT_Library library = nullptr;
|
||||
FT_Face face = nullptr;
|
||||
};
|
||||
|
||||
FontFace::FontFace(const std::string &path, int pixelSize)
|
||||
: freetype(std::make_unique<FreeType>())
|
||||
{
|
||||
if (FT_Init_FreeType(&freetype->library))
|
||||
throw std::runtime_error("FT_Init_FreeType failed");
|
||||
|
||||
if (FT_New_Face(freetype->library, path.c_str(), 0, &freetype->face))
|
||||
throw std::runtime_error("FT_New_Face failed");
|
||||
|
||||
FT_Set_Pixel_Sizes(freetype->face, 0, pixelSize);
|
||||
}
|
||||
|
||||
FontFace::~FontFace()
|
||||
{
|
||||
if (freetype->face)
|
||||
FT_Done_Face(freetype->face);
|
||||
if (freetype->library)
|
||||
FT_Done_FreeType(freetype->library);
|
||||
}
|
||||
|
||||
void *FontFace::getFace() const
|
||||
{
|
||||
return freetype->face;
|
||||
}
|
||||
} // namespace Display::UI::Text
|
||||
@@ -1,25 +0,0 @@
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace Display::UI::Text
|
||||
{
|
||||
class FontFace
|
||||
{
|
||||
public:
|
||||
FontFace(const std::string &path, int pixelSize);
|
||||
~FontFace();
|
||||
|
||||
FontFace(const FontFace &) = delete;
|
||||
FontFace &operator=(const FontFace &) = delete;
|
||||
|
||||
FontFace(FontFace &&) = delete;
|
||||
FontFace &operator=(FontFace &&) = delete;
|
||||
|
||||
void *getFace() const;
|
||||
|
||||
private:
|
||||
struct FreeType;
|
||||
std::unique_ptr<FreeType> freetype;
|
||||
};
|
||||
} // namespace Display::UI::Text
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "Display/UI/Text/Fonts.h"
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
#include <stdexcept>
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
#include "Helpers/Paths.h"
|
||||
|
||||
namespace Display::UI::Text
|
||||
{
|
||||
struct Fonts::Library
|
||||
{
|
||||
FT_Library library = nullptr;
|
||||
};
|
||||
struct Fonts::Face
|
||||
{
|
||||
FT_Face face = nullptr;
|
||||
};
|
||||
|
||||
Fonts::Fonts()
|
||||
: library(std::make_unique<Library>())
|
||||
{
|
||||
if (FT_Init_FreeType(&library->library))
|
||||
throw std::runtime_error("FT_Init_FreeType failed");
|
||||
|
||||
loadAllFonts();
|
||||
}
|
||||
|
||||
Fonts::~Fonts()
|
||||
{
|
||||
for (auto &[k, v] : fonts)
|
||||
if (v->face)
|
||||
FT_Done_Face(v->face);
|
||||
if (library->library)
|
||||
FT_Done_FreeType(library->library);
|
||||
}
|
||||
|
||||
void Fonts::loadAllFonts()
|
||||
{
|
||||
loadFonts("/usr/share/fonts/truetype");
|
||||
loadFonts(Helpers::initPaths().exeDir);
|
||||
}
|
||||
|
||||
void Fonts::loadFonts(const std::string &path)
|
||||
{
|
||||
if (std::filesystem::exists(path))
|
||||
{
|
||||
for (auto &p : std::filesystem::recursive_directory_iterator(path))
|
||||
{
|
||||
if (p.path().extension() == ".ttf" || p.path().extension() == ".otf")
|
||||
{
|
||||
std::string path = p.path().string();
|
||||
std::string filename = p.path().stem().string();
|
||||
|
||||
// Можно сразу создавать Fonts для стандартных размеров, например: 10,12,14,16,18,24
|
||||
for (int sz : {5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 24, 36, 72})
|
||||
{
|
||||
Font key{filename, sz};
|
||||
|
||||
std::unique_ptr<Face> face = std::make_unique<Face>();
|
||||
if (FT_New_Face(library->library, path.c_str(), 0, &face->face))
|
||||
throw std::runtime_error("FT_New_Face failed");
|
||||
FT_Set_Pixel_Sizes(face->face, 0, sz);
|
||||
|
||||
fonts.emplace(key, std::move(face));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Glyph &Fonts::getGlyph(char ch, const Font &font)
|
||||
{
|
||||
|
||||
GlyphKey key{ch, font};
|
||||
|
||||
auto gph = glyphs.find(key);
|
||||
if (gph != glyphs.end())
|
||||
return gph->second;
|
||||
|
||||
auto fnt = fonts.find(font);
|
||||
if (fnt == fonts.end())
|
||||
{
|
||||
// Если нет, можно использовать какой-нибудь дефолтный шрифт
|
||||
fnt = fonts.begin();
|
||||
}
|
||||
|
||||
FT_Face face = fnt->second->face;
|
||||
|
||||
if (!face)
|
||||
throw std::runtime_error("Fonts not initialized!");
|
||||
if (FT_Load_Char(face, ch, FT_LOAD_RENDER | FT_LOAD_TARGET_MONO))
|
||||
throw std::runtime_error("FT_Load_Char failed");
|
||||
FT_GlyphSlot g = face->glyph;
|
||||
|
||||
Glyph glyph;
|
||||
glyph.width = g->bitmap.width;
|
||||
glyph.height = g->bitmap.rows;
|
||||
glyph.bearingX = g->bitmap_left;
|
||||
glyph.bearingY = g->bitmap_top;
|
||||
glyph.advance = g->advance.x >> 6;
|
||||
// glyph.buffer.assign(g->bitmap.buffer, g->bitmap.buffer + g->bitmap.width * g->bitmap.rows);
|
||||
|
||||
glyph.buffer.resize(glyph.width * glyph.height, 0);
|
||||
|
||||
const FT_Bitmap &bm = g->bitmap;
|
||||
for (int y = 0; y < bm.rows; ++y)
|
||||
{
|
||||
for (int x = 0; x < bm.width; ++x)
|
||||
{
|
||||
int byteIndex = y * bm.pitch + x / 8;
|
||||
int bitIndex = 7 - (x % 8);
|
||||
uint8_t bit = (bm.buffer[byteIndex] >> bitIndex) & 1;
|
||||
glyph.buffer[y * bm.width + x] = bit ? 255 : 0;
|
||||
}
|
||||
}
|
||||
return glyphs.emplace(key, std::move(glyph)).first->second;
|
||||
}
|
||||
} // namespace Display::UI::Text
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include "Display/UI/Text/Font.h"
|
||||
#include "Display/UI/Text/Glyph.h"
|
||||
#include "Display/UI/Text/GlyphKey.h"
|
||||
|
||||
namespace Display::UI::Text
|
||||
{
|
||||
class Fonts
|
||||
{
|
||||
public:
|
||||
Fonts();
|
||||
~Fonts();
|
||||
|
||||
const Glyph &getGlyph(char ch, const Font &key);
|
||||
|
||||
private:
|
||||
struct Library;
|
||||
struct Face;
|
||||
std::unique_ptr<Library> library;
|
||||
std::unordered_map<Font, std::unique_ptr<Face>, FontHash> fonts;
|
||||
std::unordered_map<GlyphKey, Glyph, GlyphKeyHash> glyphs;
|
||||
void loadAllFonts();
|
||||
void loadFonts(const std::string &path);
|
||||
};
|
||||
} // namespace Display::UI::Text
|
||||
@@ -1,35 +0,0 @@
|
||||
#include "Display/UI/Text/GlyphCache.h"
|
||||
#include <stdexcept>
|
||||
#include <iostream>
|
||||
#include "Display/UI/Text/FontFace.h"
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
|
||||
namespace Display::UI::Text
|
||||
{
|
||||
GlyphCache::GlyphCache(const FontFace &fontFace) : fontFace(fontFace) {}
|
||||
|
||||
const Glyph &GlyphCache::getGlyph(char c)
|
||||
{
|
||||
FT_Face face = static_cast<FT_Face>(fontFace.getFace());
|
||||
auto it = cache.find(c);
|
||||
if (it != cache.end())
|
||||
return it->second;
|
||||
|
||||
if (!face)
|
||||
throw std::runtime_error("FontFace not initialized!");
|
||||
if (FT_Load_Char(face, c, FT_LOAD_RENDER))
|
||||
throw std::runtime_error("FT_Load_Char failed");
|
||||
FT_GlyphSlot g = face->glyph;
|
||||
|
||||
Glyph glyph;
|
||||
glyph.width = g->bitmap.width;
|
||||
glyph.height = g->bitmap.rows;
|
||||
glyph.bearingX = g->bitmap_left;
|
||||
glyph.bearingY = g->bitmap_top;
|
||||
glyph.advance = g->advance.x >> 6;
|
||||
glyph.buffer.assign(g->bitmap.buffer, g->bitmap.buffer + g->bitmap.width * g->bitmap.rows);
|
||||
|
||||
return cache.emplace(c, std::move(glyph)).first->second;
|
||||
}
|
||||
} // namespace Display::UI::Text
|
||||
@@ -1,28 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Display/UI/Text/Glyph.h"
|
||||
|
||||
#include <unordered_map>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Display::UI::Text
|
||||
{
|
||||
class FontFace;
|
||||
class GlyphCache
|
||||
{
|
||||
public:
|
||||
GlyphCache(const FontFace &fontFace);
|
||||
|
||||
GlyphCache(const GlyphCache &) = delete;
|
||||
GlyphCache &operator=(const GlyphCache &) = delete;
|
||||
|
||||
GlyphCache(GlyphCache &&) = delete;
|
||||
GlyphCache &operator=(GlyphCache &&) = delete;
|
||||
|
||||
const Glyph &getGlyph(char c);
|
||||
|
||||
private:
|
||||
const FontFace &fontFace;
|
||||
std::unordered_map<char, Glyph> cache;
|
||||
};
|
||||
} // namespace Display::UI::Text
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include "Font.h"
|
||||
|
||||
namespace Display::UI::Text
|
||||
{
|
||||
struct GlyphKey
|
||||
{
|
||||
char ch;
|
||||
Font font;
|
||||
|
||||
bool operator==(const GlyphKey &other) const
|
||||
{
|
||||
return ch == other.ch && font == other.font;
|
||||
}
|
||||
};
|
||||
|
||||
struct GlyphKeyHash
|
||||
{
|
||||
std::size_t operator()(const GlyphKey &k) const
|
||||
{
|
||||
std::size_t h1 = std::hash<char>()(k.ch);
|
||||
std::size_t h2 = FontHash{}(k.font);
|
||||
return h1 ^ (h2 << 1);
|
||||
}
|
||||
};
|
||||
} // namespace Display::UI::Text
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "Display/UI/Text/Helpers.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
namespace Display::UI::Text
|
||||
{
|
||||
std::string formatFloat(float value, int decimals)
|
||||
{
|
||||
char buffer[64];
|
||||
std::snprintf(buffer, sizeof(buffer), "%.*f", decimals, value);
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
std::string formatDouble(double value, int decimals)
|
||||
{
|
||||
char buffer[64];
|
||||
std::snprintf(buffer, sizeof(buffer), "%.*f", decimals, value);
|
||||
return std::string(buffer);
|
||||
}
|
||||
} // namespace Display::UI::Text
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Display::UI::Text
|
||||
{
|
||||
std::string formatFloat(float value, int decimals = 1);
|
||||
std::string formatDouble(double value, int decimals = 1);
|
||||
} // namespace Display::UI::Text
|
||||
@@ -5,18 +5,15 @@
|
||||
|
||||
namespace Display::UI::Text
|
||||
{
|
||||
|
||||
Renderer::Renderer() : fontFace("/usr/share/fonts/truetype/noto/NotoMono-Regular.ttf", 12), glyphCache(fontFace) {}
|
||||
|
||||
void Renderer::drawText(Display::Graphics::Framebuffer &fb, int x, int y,
|
||||
const std::string &text, const Display::Graphics::Color &color)
|
||||
const std::string &text, const Display::Graphics::Color &color, const Font &font)
|
||||
{
|
||||
int penX = x;
|
||||
int baseline = y;
|
||||
|
||||
for (char ch : text)
|
||||
{
|
||||
const Glyph &g = glyphCache.getGlyph(ch);
|
||||
const Glyph &g = fonts.getGlyph(ch, font);
|
||||
|
||||
for (int row = 0; row < g.height; ++row)
|
||||
{
|
||||
@@ -44,4 +41,46 @@ namespace Display::UI::Text
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::drawTextOutlined(Display::Graphics::Framebuffer &fb,
|
||||
int x, int y,
|
||||
const std::string &text,
|
||||
const Display::Graphics::Color &colorText,
|
||||
const Display::Graphics::Color &colorOutline,
|
||||
const Font &font)
|
||||
{
|
||||
// outline
|
||||
for (int dx = -1; dx <= 1; ++dx)
|
||||
for (int dy = -1; dy <= 1; ++dy)
|
||||
if (dx != 0 || dy != 0)
|
||||
drawText(fb, x + dx, y + dy, text, colorOutline, font);
|
||||
|
||||
// main text
|
||||
drawText(fb, x, y, text, colorText, font);
|
||||
}
|
||||
|
||||
/* int Renderer::measureWidth(const std::string &text)
|
||||
{
|
||||
int width = 0;
|
||||
for (char ch : text)
|
||||
{
|
||||
const Glyph &g = glyphCache.getGlyph(ch, face);
|
||||
width += g.advance;
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
int Renderer::measureHeight(const std::string &text)
|
||||
{
|
||||
int height = 0;
|
||||
for (char ch : text)
|
||||
{
|
||||
const Glyph &g = glyphCache.getGlyph(ch, face);
|
||||
int glyphHeight = g.height - g.bearingY + g.bearingY; // можно уточнить по высоте глифа
|
||||
if (glyphHeight > height)
|
||||
height = glyphHeight;
|
||||
}
|
||||
return height;
|
||||
}
|
||||
*/
|
||||
|
||||
} // namespace Display::UI::Text
|
||||
@@ -1,11 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "Display/Graphics/Framebuffer.h"
|
||||
#include "Display/UI/Text/FontFace.h"
|
||||
#include "Display/UI/Text/GlyphCache.h"
|
||||
#include "Display/UI/Text/Fonts.h"
|
||||
#include "Display/UI/Text/Font.h"
|
||||
#include "Display/Graphics/Color.h"
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Display::UI::Text
|
||||
{
|
||||
@@ -13,16 +14,14 @@ namespace Display::UI::Text
|
||||
class Renderer
|
||||
{
|
||||
public:
|
||||
Renderer();
|
||||
void drawText(Display::Graphics::Framebuffer &fb, int x, int y, const std::string &text, const Display::Graphics::Color &color = Display::Graphics::Color{255, 255, 255}, const Font &font = {"LiberationSans-Regular", 12});
|
||||
void drawTextOutlined(Display::Graphics::Framebuffer &fb, int x, int y, const std::string &text, const Display::Graphics::Color &colorText = Display::Graphics::Color{255, 255, 255}, const Display::Graphics::Color &colorOutline = Display::Graphics::Color{0, 0, 0}, const Font &font = {"LiberationSans-Regular", 12});
|
||||
|
||||
Renderer(const Renderer &) = delete;
|
||||
Renderer &operator=(const Renderer &) = delete;
|
||||
|
||||
void drawText(Display::Graphics::Framebuffer &fb, int x, int y, const std::string &text, const Display::Graphics::Color &color);
|
||||
// int measureWidth(const std::string &text);
|
||||
// int measureHeight(const std::string &text);
|
||||
|
||||
private:
|
||||
FontFace fontFace;
|
||||
GlyphCache glyphCache;
|
||||
Fonts fonts;
|
||||
};
|
||||
|
||||
} // namespace Display::UI::Text
|
||||
@@ -0,0 +1,15 @@
|
||||
message(STATUS "···Configuring Theme")
|
||||
|
||||
add_library(Theme INTERFACE)
|
||||
|
||||
add_library(Display::UI::Theme ALIAS Theme)
|
||||
|
||||
target_link_libraries(Theme
|
||||
INTERFACE
|
||||
Display::Graphics
|
||||
)
|
||||
|
||||
target_include_directories(Theme
|
||||
INTERFACE
|
||||
${INCLUDE_BASE_DIR}
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "Display/Graphics/Color.h"
|
||||
|
||||
namespace Display::UI::Theme
|
||||
{
|
||||
namespace Text
|
||||
{
|
||||
const Display::Graphics::Color TEXT{220, 220, 220};
|
||||
const Display::Graphics::Color OUTLINE{20, 20, 20};
|
||||
} // namespace Text
|
||||
|
||||
namespace Header
|
||||
{
|
||||
// ===== Header =====
|
||||
const Display::Graphics::Color BACKGROUND{20, 20, 20};
|
||||
const Display::Graphics::Color BORDER{60, 60, 60};
|
||||
const Display::Graphics::Color HEADER{30, 30, 30};
|
||||
} // namespace Header
|
||||
|
||||
namespace HostBlock
|
||||
{
|
||||
// ===== HostBlock =====
|
||||
const Display::Graphics::Color BACKGROUND{20, 20, 20};
|
||||
const Display::Graphics::Color BORDER{60, 60, 60};
|
||||
const Display::Graphics::Color HEADER{30, 30, 30};
|
||||
} // namespace HostBlock
|
||||
|
||||
namespace Bar
|
||||
{
|
||||
const Display::Graphics::Color BACKGROUND{30, 30, 30};
|
||||
const Display::Graphics::Color FILL{0, 180, 0};
|
||||
const Display::Graphics::Color BORDER{80, 80, 80};
|
||||
} // namespace Bar
|
||||
} // namespace Display::UI::Theme
|
||||
@@ -5,12 +5,12 @@ configure_file(
|
||||
${CMAKE_CURRENT_BINARY_DIR}/Version.h
|
||||
)
|
||||
|
||||
add_library(Helpers INTERFACE)
|
||||
add_library(Helpers Paths.cpp)
|
||||
|
||||
add_library(Helpers::All ALIAS Helpers)
|
||||
|
||||
target_include_directories(Helpers
|
||||
INTERFACE
|
||||
PUBLIC
|
||||
${INCLUDE_BASE_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <unistd.h>
|
||||
#include <limits.h>
|
||||
|
||||
namespace Helpers
|
||||
{
|
||||
std::filesystem::path getExecutablePath()
|
||||
{
|
||||
char buf[PATH_MAX];
|
||||
ssize_t len = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
|
||||
if (len == -1)
|
||||
throw std::runtime_error("readlink(/proc/self/exe) failed");
|
||||
|
||||
buf[len] = '\0';
|
||||
return std::filesystem::path(buf);
|
||||
}
|
||||
|
||||
struct Paths
|
||||
{
|
||||
std::filesystem::path exe;
|
||||
std::filesystem::path exeDir;
|
||||
std::filesystem::path configDir;
|
||||
std::filesystem::path assetsDir;
|
||||
};
|
||||
|
||||
Paths initPaths()
|
||||
{
|
||||
Paths p;
|
||||
p.exe = getExecutablePath();
|
||||
p.exeDir = p.exe.parent_path();
|
||||
p.configDir = p.exeDir / "config";
|
||||
p.assetsDir = p.exeDir / "assets";
|
||||
return p;
|
||||
}
|
||||
} // namespace Helpers
|
||||
+26
-11
@@ -5,6 +5,8 @@
|
||||
#include <sys/statvfs.h>
|
||||
#include <unistd.h>
|
||||
#include <limits.h>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
|
||||
namespace Metrics
|
||||
{
|
||||
@@ -99,36 +101,49 @@ namespace Metrics
|
||||
uint64_t value;
|
||||
std::string unit;
|
||||
|
||||
uint64_t total = 0, available = 0;
|
||||
uint64_t mem_total = 0, mem_available = 0, swap_total = 0, swap_available = 0;
|
||||
|
||||
while (f >> key >> value >> unit)
|
||||
{
|
||||
if (key == "MemTotal:")
|
||||
total = value * 1024;
|
||||
mem_total = value * 1024;
|
||||
else if (key == "MemAvailable:")
|
||||
available = value * 1024;
|
||||
mem_available = value * 1024;
|
||||
else if (key == "SwapTotal:")
|
||||
swap_total = value * 1024;
|
||||
else if (key == "SwapFree:")
|
||||
swap_available = value * 1024;
|
||||
}
|
||||
|
||||
Memory memory;
|
||||
memory.total = total;
|
||||
memory.available = available;
|
||||
memory.used = total - available;
|
||||
memory.mem_total = mem_total;
|
||||
memory.mem_available = mem_available;
|
||||
memory.mem_used = mem_total - mem_available;
|
||||
memory.swap_total = swap_total;
|
||||
memory.swap_available = swap_available;
|
||||
memory.swap_used = swap_total - swap_available;
|
||||
return memory;
|
||||
}
|
||||
|
||||
Disk Collector::readDisk(const char *path)
|
||||
{
|
||||
struct statvfs vfs{};
|
||||
statvfs(path, &vfs);
|
||||
struct statvfs vfs;
|
||||
if (statvfs(path, &vfs) != 0)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
std::string("statvfs failed for ") + path + ": " +
|
||||
std::strerror(errno));
|
||||
}
|
||||
|
||||
uint64_t total = vfs.f_blocks * vfs.f_frsize;
|
||||
uint64_t free = vfs.f_bavail * vfs.f_frsize;
|
||||
uint64_t total = static_cast<uint64_t>(vfs.f_blocks) * vfs.f_frsize;
|
||||
uint64_t free = static_cast<uint64_t>(vfs.f_bavail) * vfs.f_frsize;
|
||||
uint64_t used = total - free;
|
||||
|
||||
Disk d;
|
||||
d.name = path;
|
||||
d.total = total;
|
||||
d.free = free;
|
||||
d.used = total - free;
|
||||
d.used = used;
|
||||
return d;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,8 +41,10 @@ namespace Metrics
|
||||
buf.writeFloat(f);
|
||||
|
||||
// Memory
|
||||
buf.writeFloat(memory.used);
|
||||
buf.writeFloat(memory.total);
|
||||
buf.writeFloat(memory.mem_used);
|
||||
buf.writeFloat(memory.mem_total);
|
||||
buf.writeFloat(memory.swap_used);
|
||||
buf.writeFloat(memory.swap_total);
|
||||
|
||||
// Disks
|
||||
buf.writeUint8(static_cast<uint8_t>(disks.size()));
|
||||
@@ -77,8 +79,10 @@ namespace Metrics
|
||||
h.cpu.coreLoads.push_back(buf.readFloat());
|
||||
|
||||
// Memory
|
||||
h.memory.used = buf.readFloat();
|
||||
h.memory.total = buf.readFloat();
|
||||
h.memory.mem_used = buf.readFloat();
|
||||
h.memory.mem_total = buf.readFloat();
|
||||
h.memory.swap_used = buf.readFloat();
|
||||
h.memory.swap_total = buf.readFloat();
|
||||
|
||||
// Disks
|
||||
uint8_t numDisks = buf.readUint8();
|
||||
|
||||
@@ -4,8 +4,11 @@ namespace Metrics
|
||||
{
|
||||
struct Memory
|
||||
{
|
||||
float used;
|
||||
float available;
|
||||
float total;
|
||||
float mem_used;
|
||||
float mem_available;
|
||||
float mem_total;
|
||||
float swap_used;
|
||||
float swap_total;
|
||||
float swap_available;
|
||||
};
|
||||
} // namespace Metrics
|
||||
|
||||
@@ -4,13 +4,13 @@ namespace Model
|
||||
{
|
||||
void HostRegistry::update(const std::string &host, const Metrics::Host &m)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
hosts_[host] = m;
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
hosts[host] = m;
|
||||
}
|
||||
|
||||
std::map<std::string, Metrics::Host> HostRegistry::snapshot()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return hosts_;
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
return hosts;
|
||||
}
|
||||
} // namespace Model
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Model
|
||||
std::map<std::string, Metrics::Host> snapshot();
|
||||
|
||||
private:
|
||||
std::map<std::string, Metrics::Host> hosts_;
|
||||
std::mutex mutex_;
|
||||
std::map<std::string, Metrics::Host> hosts;
|
||||
std::mutex mutex;
|
||||
};
|
||||
} // namespace Model
|
||||
|
||||
Reference in New Issue
Block a user