initial commit
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
#include "log_viewer_page.h"
|
||||
#include "app_state.h"
|
||||
#include "timezone_helper.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
namespace {
|
||||
|
||||
struct WrappedLogLine {
|
||||
std::string text;
|
||||
SDL_Color color;
|
||||
};
|
||||
|
||||
std::vector<std::string> wrapText(TTF_Font* font, const std::string& text, float maxW) {
|
||||
std::vector<std::string> result;
|
||||
if (!font || text.empty() || maxW <= 0) return result;
|
||||
|
||||
std::stringstream ss(text);
|
||||
std::string word;
|
||||
std::string currentLine;
|
||||
|
||||
while (ss >> word) {
|
||||
std::string testLine = currentLine.empty() ? word : currentLine + " " + word;
|
||||
|
||||
int w = 0, h = 0;
|
||||
TTF_GetStringSize(font, testLine.c_str(), 0, &w, &h);
|
||||
|
||||
if (static_cast<float>(w) <= maxW) {
|
||||
currentLine = testLine;
|
||||
} else {
|
||||
if (!currentLine.empty()) {
|
||||
result.push_back(currentLine);
|
||||
}
|
||||
currentLine = word;
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentLine.empty()) {
|
||||
result.push_back(currentLine);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* filterLevelToString(LogViewerPage::FilterLevel lvl) {
|
||||
switch (lvl) {
|
||||
case LogViewerPage::FilterLevel::All: return "ALL";
|
||||
case LogViewerPage::FilterLevel::Debug: return "DEBUG";
|
||||
case LogViewerPage::FilterLevel::Info: return "INFO";
|
||||
case LogViewerPage::FilterLevel::Warning: return "WARNING";
|
||||
case LogViewerPage::FilterLevel::Error: return "ERROR";
|
||||
case LogViewerPage::FilterLevel::Fatal: return "FATAL";
|
||||
}
|
||||
return "ALL";
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void LogViewerPage::onPush(AppState& state) {
|
||||
state_ = &state;
|
||||
TTF_Init();
|
||||
createFonts();
|
||||
currentTzCode_ = state.config.get().timezone;
|
||||
refreshLogs();
|
||||
}
|
||||
|
||||
void LogViewerPage::onResume(AppState& state) {
|
||||
state_ = &state;
|
||||
createFonts();
|
||||
currentTzCode_ = state.config.get().timezone;
|
||||
refreshLogs();
|
||||
}
|
||||
|
||||
void LogViewerPage::onPause(AppState& state) {
|
||||
destroyFonts();
|
||||
destroyTextures();
|
||||
}
|
||||
|
||||
void LogViewerPage::onPop(AppState& state) {
|
||||
destroyFonts();
|
||||
destroyTextures();
|
||||
TTF_Quit();
|
||||
}
|
||||
|
||||
void LogViewerPage::refreshLogs() {
|
||||
using namespace radiantwave::logger;
|
||||
|
||||
if (filterLevel_ == FilterLevel::All) {
|
||||
rawEntries_ = Logger::instance().getEntries();
|
||||
} else {
|
||||
LogLevel targetLvl = LogLevel::Info;
|
||||
if (filterLevel_ == FilterLevel::Debug) targetLvl = LogLevel::Debug;
|
||||
else if (filterLevel_ == FilterLevel::Info) targetLvl = LogLevel::Info;
|
||||
else if (filterLevel_ == FilterLevel::Warning) targetLvl = LogLevel::Warning;
|
||||
else if (filterLevel_ == FilterLevel::Error) targetLvl = LogLevel::Error;
|
||||
else if (filterLevel_ == FilterLevel::Fatal) targetLvl = LogLevel::Fatal;
|
||||
|
||||
rawEntries_ = Logger::instance().getEntriesFiltered(targetLvl, true);
|
||||
}
|
||||
|
||||
if (sortOrder_ == SortOrder::NewestFirst) {
|
||||
std::sort(rawEntries_.begin(), rawEntries_.end(), [](const LogEntry& a, const LogEntry& b) {
|
||||
return a.timestamp > b.timestamp;
|
||||
});
|
||||
} else {
|
||||
std::sort(rawEntries_.begin(), rawEntries_.end(), [](const LogEntry& a, const LogEntry& b) {
|
||||
return a.timestamp < b.timestamp;
|
||||
});
|
||||
}
|
||||
|
||||
wrappedDirty_ = true;
|
||||
}
|
||||
|
||||
void LogViewerPage::handleEvent(const SDL_Event& event) {
|
||||
if (!state_) return;
|
||||
|
||||
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||
SDL_Keycode key = event.key.key;
|
||||
|
||||
if (key == SDLK_L || key == SDLK_F2 || key == SDLK_RIGHT) {
|
||||
filterLevel_ = static_cast<FilterLevel>((static_cast<int>(filterLevel_) + 1) % 6);
|
||||
scrollOffset_ = 0;
|
||||
refreshLogs();
|
||||
} else if (key == SDLK_LEFT) {
|
||||
filterLevel_ = static_cast<FilterLevel>((static_cast<int>(filterLevel_) - 1 + 6) % 6);
|
||||
scrollOffset_ = 0;
|
||||
refreshLogs();
|
||||
} else if (key == SDLK_S || key == SDLK_F3) {
|
||||
sortOrder_ = (sortOrder_ == SortOrder::NewestFirst) ? SortOrder::OldestFirst : SortOrder::NewestFirst;
|
||||
scrollOffset_ = 0;
|
||||
refreshLogs();
|
||||
} else if (key == SDLK_C) {
|
||||
radiantwave::logger::Logger::instance().clear();
|
||||
scrollOffset_ = 0;
|
||||
refreshLogs();
|
||||
} else if (key == SDLK_UP) {
|
||||
if (scrollOffset_ > 0) scrollOffset_--;
|
||||
} else if (key == SDLK_DOWN) {
|
||||
scrollOffset_++;
|
||||
} else if (key == SDLK_PAGEUP) {
|
||||
scrollOffset_ = std::max(0, scrollOffset_ - 10);
|
||||
} else if (key == SDLK_PAGEDOWN) {
|
||||
scrollOffset_ += 10;
|
||||
} else if (key == SDLK_ESCAPE || key == SDLK_TAB) {
|
||||
state_->pages.pop(*state_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LogViewerPage::update(float dt) {
|
||||
updateTimer_ += dt;
|
||||
if (updateTimer_ >= 0.25f) {
|
||||
updateTimer_ = 0.0f;
|
||||
refreshLogs();
|
||||
}
|
||||
}
|
||||
|
||||
void LogViewerPage::render(SDL_Renderer* renderer) {
|
||||
int winW, winH;
|
||||
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH);
|
||||
float fw = static_cast<float>(winW);
|
||||
float fh = static_cast<float>(winH);
|
||||
|
||||
// Background
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderDrawColor(renderer, 15, 18, 25, 255);
|
||||
SDL_FRect bgRect{0, 0, fw, fh};
|
||||
SDL_RenderFillRect(renderer, &bgRect);
|
||||
|
||||
if (!fontTitle_ || !fontHeader_ || !fontLog_ || !fontSmall_) return;
|
||||
|
||||
SDL_Color white = {255, 255, 255, 255};
|
||||
SDL_Color cyan = {100, 220, 255, 255};
|
||||
SDL_Color gray = {160, 160, 160, 255};
|
||||
|
||||
float currentY = 25.0f;
|
||||
|
||||
// 1. Title
|
||||
titleText_.ensure(renderer, fontTitle_, "System Event Log Viewer", white);
|
||||
if (titleText_.texture) {
|
||||
float x = (fw - titleText_.width) / 2.0f;
|
||||
SDL_FRect dst{x, currentY, titleText_.width, titleText_.height};
|
||||
SDL_RenderTexture(renderer, titleText_.texture, nullptr, &dst);
|
||||
currentY += 55.0f;
|
||||
}
|
||||
|
||||
// 2. Control & Status Header Bar
|
||||
std::string filterStr = "Filter: " + std::string(filterLevelToString(filterLevel_));
|
||||
std::string sortStr = "Sort: " + std::string((sortOrder_ == SortOrder::NewestFirst) ? "Newest First" : "Oldest First");
|
||||
std::string countStr = "Entries: " + std::to_string(rawEntries_.size());
|
||||
std::string headerInfo = filterStr + " | " + sortStr + " | " + countStr + " | TZ: " + currentTzCode_;
|
||||
|
||||
SDL_SetRenderDrawColor(renderer, 35, 42, 60, 255);
|
||||
SDL_FRect barRect{40.0f, currentY, fw - 80.0f, 40.0f};
|
||||
SDL_RenderFillRect(renderer, &barRect);
|
||||
|
||||
headerText_.ensure(renderer, fontHeader_, headerInfo, cyan);
|
||||
if (headerText_.texture) {
|
||||
SDL_FRect dst{55.0f, currentY + (40.0f - headerText_.height) / 2.0f, headerText_.width, headerText_.height};
|
||||
SDL_RenderTexture(renderer, headerText_.texture, nullptr, &dst);
|
||||
}
|
||||
currentY += 55.0f;
|
||||
|
||||
// 3. Render Log Entries with Full Screen Word-Wrapping. The wrapped text
|
||||
// is recomputed only when the entries or the window width change; the
|
||||
// per-line textures are cached and re-rasterized only when a line's text
|
||||
// or color changes.
|
||||
float maxLineWidth = fw - 80.0f;
|
||||
if (wrappedDirty_ || maxLineWidth != lastMaxLineWidth_) {
|
||||
rebuildWrapped(maxLineWidth);
|
||||
lastMaxLineWidth_ = maxLineWidth;
|
||||
wrappedDirty_ = false;
|
||||
}
|
||||
|
||||
int visibleLineCount = static_cast<int>((fh - currentY - 50.0f) / 26.0f);
|
||||
int maxScroll = std::max(0, static_cast<int>(wrappedTexts_.size()) - visibleLineCount);
|
||||
if (scrollOffset_ > maxScroll) scrollOffset_ = maxScroll;
|
||||
|
||||
if (logLineCache_.size() != wrappedTexts_.size()) logLineCache_.resize(wrappedTexts_.size());
|
||||
|
||||
float lineY = currentY;
|
||||
for (size_t i = scrollOffset_; i < wrappedTexts_.size(); ++i) {
|
||||
if (lineY + 26.0f > fh - 45.0f) break;
|
||||
|
||||
const auto& wline = wrappedTexts_[i];
|
||||
CachedText& cached = logLineCache_[i];
|
||||
cached.ensure(renderer, fontLog_, wline.text, wline.color);
|
||||
if (cached.texture) {
|
||||
SDL_FRect dst{40.0f, lineY, cached.width, cached.height};
|
||||
SDL_RenderTexture(renderer, cached.texture, nullptr, &dst);
|
||||
}
|
||||
|
||||
lineY += 26.0f;
|
||||
}
|
||||
|
||||
// 4. Footer Help Line
|
||||
footerText_.ensure(renderer, fontSmall_,
|
||||
"[UP/DOWN] Scroll [L/F2] Filter Level [S/F3] Sort Order [C] Clear Logs [ESC/TAB] Back", gray);
|
||||
if (footerText_.texture) {
|
||||
float x = (fw - footerText_.width) / 2.0f;
|
||||
SDL_FRect dst{x, fh - 35.0f, footerText_.width, footerText_.height};
|
||||
SDL_RenderTexture(renderer, footerText_.texture, nullptr, &dst);
|
||||
}
|
||||
}
|
||||
|
||||
void LogViewerPage::rebuildWrapped(float maxLineWidth) {
|
||||
wrappedTexts_.clear();
|
||||
if (!fontLog_) return;
|
||||
|
||||
using namespace radiantwave::logger;
|
||||
|
||||
for (const auto& entry : rawEntries_) {
|
||||
std::string timeStr = radiantwave::timezone::formatUtcToTimezone(entry.timestamp, currentTzCode_, "%Y-%m-%d %H:%M:%S");
|
||||
|
||||
std::string fullLineStr = "[" + timeStr + "] [" + std::string(logLevelToString(entry.level)) + "] [" + entry.component + "] " + entry.message;
|
||||
|
||||
SDL_Color levelColor = {255, 255, 255, 255};
|
||||
if (entry.level == LogLevel::Debug) levelColor = {160, 160, 160, 255};
|
||||
else if (entry.level == LogLevel::Info) levelColor = {255, 255, 255, 255};
|
||||
else if (entry.level == LogLevel::Warning) levelColor = {255, 200, 0, 255};
|
||||
else if (entry.level == LogLevel::Error) levelColor = {255, 80, 80, 255};
|
||||
else if (entry.level == LogLevel::Fatal) levelColor = {255, 60, 180, 255};
|
||||
|
||||
auto subLines = wrapText(fontLog_, fullLineStr, maxLineWidth);
|
||||
for (const auto& sub : subLines) {
|
||||
wrappedTexts_.push_back({sub, levelColor});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LogViewerPage::createFonts() {
|
||||
if (fontTitle_) return;
|
||||
fontTitle_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 48);
|
||||
fontHeader_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 26);
|
||||
fontLog_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 22);
|
||||
fontSmall_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 20);
|
||||
}
|
||||
|
||||
void LogViewerPage::destroyFonts() {
|
||||
if (fontTitle_) { TTF_CloseFont(fontTitle_); fontTitle_ = nullptr; }
|
||||
if (fontHeader_) { TTF_CloseFont(fontHeader_); fontHeader_ = nullptr; }
|
||||
if (fontLog_) { TTF_CloseFont(fontLog_); fontLog_ = nullptr; }
|
||||
if (fontSmall_) { TTF_CloseFont(fontSmall_); fontSmall_ = nullptr; }
|
||||
}
|
||||
|
||||
void LogViewerPage::destroyTextures() {
|
||||
titleText_.destroy();
|
||||
headerText_.destroy();
|
||||
footerText_.destroy();
|
||||
for (auto& cached : logLineCache_) cached.destroy();
|
||||
logLineCache_.clear();
|
||||
wrappedTexts_.clear();
|
||||
wrappedDirty_ = true;
|
||||
lastMaxLineWidth_ = -1.0f;
|
||||
}
|
||||
Reference in New Issue
Block a user