This repository has been archived on 2025-03-20. You can view files and clone it, but cannot push or open issues or pull requests.
The-Powder-Toy/src/gui/font/FontEditor.cpp
Saveliy Skresanov 0165a9b121
Fix a bug in the font editor where the last row of a symbol is cleared
when shifting the whole symbol up.
2023-10-08 12:25:14 -04:00

634 lines
18 KiB
C++

#include "FontEditor.h"
#include "bzip2/bz2wrap.h"
#include "gui/interface/Textbox.h"
#include "gui/interface/Engine.h"
#include "gui/interface/Point.h"
#include "gui/interface/Button.h"
#include "gui/interface/ScrollPanel.h"
#include "graphics/Graphics.h"
#include "SimulationConfig.h"
#include <stdexcept>
#include <fstream>
#include <iterator>
#include <iomanip>
#include <iostream>
#include <cstdint>
#include <SDL.h>
extern unsigned char *font_data;
extern unsigned int *font_ptrs;
extern unsigned int (*font_ranges)[2];
void FontEditor::ReadDataFile(ByteString dataFile)
{
std::fstream file;
file.open(dataFile, std::ios_base::in | std::ios_base::binary);
if(!file)
throw std::runtime_error("Could not open " + dataFile);
file.seekg(0, std::ios_base::end);
std::vector<char> fileData(file.tellg());
file.seekg(0);
file.read(&fileData[0], fileData.size());
file.close();
std::vector<char> fontDataBuf;
std::vector<int> fontPtrsBuf;
std::vector< std::array<int, 2> > fontRangesBuf;
if (BZ2WDecompress(fontDataBuf, fileData.data(), fileData.size()) != BZ2WDecompressOk)
{
throw std::runtime_error("Could not decompress font data");
}
int first = -1;
int last = -1;
char *begin = &fontDataBuf[0];
char *ptr = &fontDataBuf[0];
char *end = &fontDataBuf[0] + fontDataBuf.size();
while (ptr != end)
{
if (ptr + 4 > end)
{
throw std::runtime_error("Could not decompress font data");
}
auto codePoint = *reinterpret_cast<uint32_t *>(ptr) & 0xFFFFFFU;
if (codePoint >= 0x110000U)
{
throw std::runtime_error("Could not decompress font data");
}
auto width = *reinterpret_cast<uint8_t *>(ptr + 3);
if (width > 64)
{
throw std::runtime_error("Could not decompress font data");
}
if (ptr + 4 + width * 3 > end)
{
throw std::runtime_error("Could not decompress font data");
}
auto cp = (int)codePoint;
if (last >= cp)
{
throw std::runtime_error("Could not decompress font data");
}
if (first != -1 && last + 1 < cp)
{
fontRangesBuf.push_back({ { first, last } });
first = -1;
}
if (first == -1)
{
first = cp;
}
last = cp;
fontPtrsBuf.push_back(ptr + 3 - begin);
ptr += width * 3 + 4;
}
if (first != -1)
{
fontRangesBuf.push_back({ { first, last } });
}
fontRangesBuf.push_back({ { 0, 0 } });
fontData.clear();
for (auto ch : fontDataBuf)
{
fontData.push_back(ch);
}
fontPtrs.clear();
for (auto ptr : fontPtrsBuf)
{
fontPtrs.push_back(ptr);
}
fontRanges.clear();
for (auto rng : fontRangesBuf)
{
fontRanges.push_back({ { (unsigned int)rng[0], (unsigned int)rng[1] } });
}
}
void FontEditor::WriteDataFile(ByteString dataFile, std::vector<unsigned char> const &fontData, std::vector<unsigned int> const &fontPtrs, std::vector<std::array<unsigned int, 2> > const &fontRanges)
{
std::fstream file;
file.open(dataFile, std::ios_base::out | std::ios_base::trunc | std::ios_base::binary);
if(!file)
throw std::runtime_error("Could not open " + dataFile);
std::vector<char> uncompressed;
size_t pos = 0;
for (size_t i = 0; pos < fontPtrs.size() && fontRanges[i][1]; i++)
{
for (String::value_type ch = fontRanges[i][0]; ch <= fontRanges[i][1]; ch++)
{
uncompressed.push_back((char)ch);
uncompressed.push_back((char)(ch >> 8));
uncompressed.push_back((char)(ch >> 16));
auto ptr = fontPtrs[pos++];
auto width = fontData[ptr];
uncompressed.push_back(width);
for (auto j = 0; j < 3 * width; ++j)
{
uncompressed.push_back(fontData[ptr + 1 + j]);
}
}
}
std::vector<char> compressed;
if (BZ2WCompress(compressed, uncompressed.data(), uncompressed.size()) != BZ2WCompressOk)
{
throw std::runtime_error("Could not compress font data");
}
file.write(compressed.data(), compressed.size());
file.close();
}
void FontEditor::UnpackData(
std::map<String::value_type, unsigned char> &fontWidths,
std::map<String::value_type, std::array<std::array<char, MAX_WIDTH>, FONT_H> > &fontPixels,
std::vector<unsigned char> const &fontData,
std::vector<unsigned int> const &fontPtrs,
std::vector<std::array<unsigned int, 2> > const &fontRanges)
{
fontWidths.clear();
fontPixels.clear();
size_t pos = 0;
for(size_t range = 0; fontRanges[range][1]; range++)
for(String::value_type ch = fontRanges[range][0]; ch <= fontRanges[range][1]; ch++)
{
unsigned char const *pointer = &fontData[fontPtrs[pos]];
int width = fontWidths[ch] = *(pointer++);
int pixels = 0;
int data = 0;
for(int j = 0; j < FONT_H; j++)
for(int i = 0; i < width; i++)
{
if(!pixels)
{
data = *(pointer++);
pixels = 4;
}
fontPixels[ch][j][i] = data & 3;
data >>= 2;
pixels--;
}
pos++;
}
}
void FontEditor::PackData(
std::map<String::value_type, unsigned char> const &fontWidths,
std::map<String::value_type, std::array<std::array<char, MAX_WIDTH>, FONT_H> > const &fontPixels,
std::vector<unsigned char> &fontData,
std::vector<unsigned int> &fontPtrs,
std::vector<std::array<unsigned int, 2> > &fontRanges)
{
fontData.clear();
fontPtrs.clear();
fontRanges.clear();
bool first = true;
String::value_type rangeStart = 0;
String::value_type prev = 0;
for(std::map<String::value_type, unsigned char>::const_iterator it = fontWidths.begin(); it != fontWidths.end(); it++)
{
String::value_type ch = it->first;
if(first)
{
rangeStart = ch;
first = false;
}
else
if(ch != prev + 1)
{
fontRanges.push_back({rangeStart, prev});
rangeStart = ch;
}
fontPtrs.push_back(fontData.size());
fontData.push_back(it->second);
int pixels = 0;
int data = 0;
for(int j = 0; j < FONT_H; j++)
for(int i = 0; i < it->second; i++)
{
if(pixels == 4)
{
fontData.push_back(data);
pixels = 0;
data = 0;
}
data >>= 2;
data |= fontPixels.at(ch)[j][i] << 6;
pixels++;
}
if(pixels)
fontData.push_back(data);
prev = ch;
}
fontRanges.push_back({rangeStart, prev});
fontRanges.push_back({0, 0});
}
class StretchLabel: public ui::Label
{
using Label::Label;
public:
int WrappedLines() const
{
return displayTextWrapper.WrappedLines();
}
};
class StretchTextbox: public ui::Textbox
{
using Textbox::Textbox;
public:
int WrappedLines() const
{
return displayTextWrapper.WrappedLines();
}
};
constexpr int FONT_SCALE = 16;
FontEditor::FontEditor(ByteString _dataFile):
ui::Window(ui::Point(0, 0), ui::Point(WINDOWW, WINDOWH)),
dataFile(_dataFile),
currentChar(0x80),
fgR(255), fgG(255), fgB(255), bgR(0), bgG(0), bgB(0),
grid(1),
rulers(1)
{
ReadDataFile(dataFile);
UnpackData(fontWidths, fontPixels, fontData, fontPtrs, fontRanges);
font_data = fontData.data();
font_ptrs = fontPtrs.data();
font_ranges = (unsigned int (*)[2])fontRanges.data();
int baseline = 8 + FONT_H * FONT_SCALE + 4 + FONT_H + 4 + 1;
int currentX = 1;
ui::Button *prev = new ui::Button(ui::Point(currentX, baseline), ui::Point(17, 17), 0xE016);
currentX += 18;
prev->SetActionCallback({ [this] { PrevChar(); } });
AddComponent(prev);
currentCharTextbox = new ui::Textbox(ui::Point(currentX, baseline), ui::Point(31, 17));
currentX += 32;
currentCharTextbox->SetActionCallback({ [this] {
unsigned int number = currentCharTextbox->GetText().ToNumber<unsigned int>(Format::Hex(), true);
if(number <= 0x10FFFF)
currentChar = number;
} });
UpdateCharNumber();
AddComponent(currentCharTextbox);
ui::Button *next = new ui::Button(ui::Point(currentX, baseline), ui::Point(17, 17), 0xE015);
currentX += 18;
next->SetActionCallback({ [this] { NextChar(); } });
AddComponent(next);
ui::Button *shrink = new ui::Button(ui::Point(currentX, baseline), ui::Point(17, 17), "><");
currentX += 18;
shrink->SetActionCallback({ [this] { ShrinkChar(); } });
AddComponent(shrink);
ui::Button *grow = new ui::Button(ui::Point(currentX, baseline), ui::Point(17, 17), "<>");
currentX += 18;
grow->SetActionCallback({ [this] { GrowChar(); } });
AddComponent(grow);
ui::Button *add = new ui::Button(ui::Point(currentX, baseline), ui::Point(36, 17), "Add");
currentX += 37;
add->SetActionCallback({ [this] {
if (fontWidths.find(currentChar) == fontWidths.end())
{
savedButton->SetToggleState(false);
fontWidths[currentChar] = 5;
fontPixels[currentChar];
}
} });
AddComponent(add);
ui::Button *remove = new ui::Button(ui::Point(currentX, baseline), ui::Point(36, 17), "Remove");
currentX += 37;
remove->SetActionCallback({ [this] {
if (fontWidths.find(currentChar) != fontWidths.end())
{
savedButton->SetToggleState(false);
fontWidths.erase(currentChar);
fontPixels.erase(currentChar);
}
} });
AddComponent(remove);
ui::Button *showGrid = new ui::Button(ui::Point(currentX, baseline), ui::Point(32, 17), "Grid");
currentX += 33;
showGrid->SetTogglable(true);
showGrid->SetToggleState(grid);
showGrid->SetActionCallback({ [this, showGrid] {
grid = showGrid->GetToggleState();
} });
AddComponent(showGrid);
ui::Button *showRulers = new ui::Button(ui::Point(currentX, baseline), ui::Point(32, 17), "Rulers");
currentX += 33;
showRulers->SetTogglable(true);
showRulers->SetToggleState(rulers);
showRulers->SetActionCallback({ [this, showRulers] {
rulers = showRulers->GetToggleState();
} });
AddComponent(showRulers);
baseline += 18;
currentX = 1;
int *refs[6] = {&fgR, &fgG, &fgB, &bgR, &bgG, &bgB};
for(int i = 0; i < 6; i++)
{
ui::Textbox *colorComponent = new ui::Textbox(ui::Point(currentX, baseline), ui::Point(27, 17), String::Build(*refs[i]));
currentX += 28;
colorComponent->SetActionCallback({ [colorComponent, refs, i] {
*refs[i] = colorComponent->GetText().ToNumber<int>(true);
} });
AddComponent(colorComponent);
}
baseline += 18;
currentX = 1;
ui::Button *render = new ui::Button(ui::Point(currentX, baseline), ui::Point(50, 17), "Render");
currentX += 51;
render->SetActionCallback({ [this] { Render(); } });
AddComponent(render);
savedButton = new ui::Button(ui::Point(currentX, baseline), ui::Point(50, 17), "Save");
currentX += 51;
savedButton->SetTogglable(true);
savedButton->SetToggleState(true);
savedButton->SetActionCallback({ [this] { Save(); } });
AddComponent(savedButton);
baseline += 18;
ui::ScrollPanel *outputPanel = new ui::ScrollPanel(ui::Point(Size.X / 2, baseline), ui::Point(Size.X / 2, Size.Y - baseline));
AddComponent(outputPanel);
StretchLabel *outputPreview = new StretchLabel(ui::Point(0, 0), ui::Point(Size.X / 2, 0), "");
outputPreview->SetMultiline(true);
outputPreview->Appearance.HorizontalAlign = ui::Appearance::AlignLeft;
outputPreview->Appearance.VerticalAlign = ui::Appearance::AlignTop;
outputPanel->AddChild(outputPreview);
ui::ScrollPanel *inputPanel = new ui::ScrollPanel(ui::Point(0, baseline), ui::Point(Size.X / 2, Size.Y - baseline));
AddComponent(inputPanel);
StretchTextbox *inputPreview = new StretchTextbox(ui::Point(0, 0), ui::Point(Size.X / 2, 0));
inputPreview->SetMultiline(true);
inputPreview->SetInputType(ui::Textbox::Multiline);
inputPreview->Appearance.HorizontalAlign = ui::Appearance::AlignLeft;
inputPreview->Appearance.VerticalAlign = ui::Appearance::AlignTop;
auto textChangedCallback = [outputPreview, outputPanel, inputPreview, inputPanel] {
String str = inputPreview->GetText();
size_t at = 0;
StringBuilder text;
while(at < str.size())
{
unsigned int ch1, ch2;
if(str[at] != ' ')
if(String::Split split1 = str.SplitNumber(ch1, Format::Hex(), at))
{
if(str[split1.PositionAfter()] == ':')
if(String::Split split2 = str.SplitNumber(ch2, Format::Hex(), split1.PositionAfter() + 1))
{
for(unsigned int ch = ch1; ch <= ch2; ch++)
text << String::value_type(ch);
at = split2.PositionAfter();
continue;
}
text << String::value_type(ch1);
at = split1.PositionAfter();
}
else
{
text << str[at++];
}
else
at++;
}
outputPreview->SetText(text.Build());
outputPanel->InnerSize.Y = outputPreview->Size.Y = std::max(outputPreview->WrappedLines(), 1) * FONT_H + 2;
inputPanel->InnerSize.Y = inputPreview->Size.Y = std::max(inputPreview->WrappedLines(), 1) * FONT_H + 2;
};
inputPreview->SetActionCallback({ textChangedCallback });
inputPanel->AddChild(inputPreview);
StringBuilder input;
input << Format::Hex() << Format::Width(2);
for(auto p : fontRanges)
if(p[1] >= 0x20)
{
if(p[0] < 0x20)
p[0] = 0x20;
if(p[0] == p[1])
input << p[0] << "\n";
else
input << p[0] << ":" << p[1] << "\n";
}
inputPreview->SetText(input.Build());
textChangedCallback();
AddComponent(inputPreview);
}
FontEditor::FontEditor(ByteString target, ByteString source):
ui::Window(ui::Point(0, 0), ui::Point(WINDOWW, WINDOWH))
{
ReadDataFile(target);
std::map<String::value_type, unsigned char> tgtFontWidths, srcFontWidths;
std::map<String::value_type, std::array<std::array<char, MAX_WIDTH>, FONT_H> > tgtFontPixels, srcFontPixels;
UnpackData(tgtFontWidths, tgtFontPixels, fontData, fontPtrs, fontRanges);
ReadDataFile(source);
UnpackData(srcFontWidths, srcFontPixels, fontData, fontPtrs, fontRanges);
for(auto const &p : srcFontPixels)
if(tgtFontPixels.count(p.first))
{
bool same = tgtFontWidths[p.first] == srcFontWidths[p.first];
if(same)
for(int j = 0; j < FONT_H; j++)
for(int i = 0; i < tgtFontWidths[p.first]; i++)
same = same && tgtFontPixels[p.first][j][i] == srcFontPixels[p.first][j][i];
if(!same)
std::cout << "U+" << std::hex << (unsigned int)p.first << " is present in both files and is different!" << std::endl;
}
else
{
std::cout << "Adding U+" << std::hex << (unsigned int)p.first << " to the target" << std::endl;
tgtFontWidths[p.first] = srcFontWidths[p.first];
tgtFontPixels[p.first] = p.second;
}
std::vector<unsigned char> tmpFontData;
std::vector<unsigned int> tmpFontPtrs;
std::vector<std::array<unsigned int, 2> > tmpFontRanges;
PackData(tgtFontWidths, tgtFontPixels, tmpFontData, tmpFontPtrs, tmpFontRanges);
WriteDataFile(target, tmpFontData, tmpFontPtrs, tmpFontRanges);
}
void FontEditor::OnDraw()
{
Graphics *g = GetGraphics();
if(fontWidths.find(currentChar) != fontWidths.end())
{
int width = fontWidths[currentChar];
std::array<std::array<char, MAX_WIDTH>, FONT_H> const &pixels = fontPixels[currentChar];
int areaWidth = 8 + width * FONT_SCALE + 8;
g->DrawFilledRect(RectSized(Vec2{ 0, 0 }, Vec2{ areaWidth, 8 + FONT_H * FONT_SCALE + 4 + FONT_H + 4 }), RGB<uint8_t>(bgR, bgG, bgB));
for(int j = 0; j < FONT_H; j++)
for(int i = 0; i < width; i++)
g->BlendFilledRect(RectSized(Vec2{ 8 + i * FONT_SCALE, 8 + j * FONT_SCALE }, Vec2{ FONT_SCALE - grid, FONT_SCALE - grid }), RGBA<uint8_t>(fgR, fgG, fgB, pixels[j][i] * 255 / 3));
for(int j = 0; j < FONT_H; j++)
for(int i = 0; i < width; i++)
g->BlendPixel({ 8 + i, 8 + FONT_H * FONT_SCALE + 4 + j }, RGBA<uint8_t>(fgR, fgG, fgB, pixels[j][i] * 255 / 3));
if(rulers)
{
g->DrawLine({ 0, 7 + 0 * FONT_SCALE }, { areaWidth - 1, 7 + 0 * FONT_SCALE }, 0x808080_rgb);
g->DrawLine({ 0, 7 + 2 * FONT_SCALE }, { areaWidth - 1, 7 + 2 * FONT_SCALE }, 0x808080_rgb);
g->DrawLine({ 0, 7 + 4 * FONT_SCALE }, { areaWidth - 1, 7 + 4 * FONT_SCALE }, 0x808080_rgb);
g->DrawLine({ 0, 7 + 9 * FONT_SCALE }, { areaWidth - 1, 7 + 9 * FONT_SCALE }, 0x808080_rgb);
g->DrawLine({ 0, 7 + 12 * FONT_SCALE }, { areaWidth - 1, 7 + 12 * FONT_SCALE }, 0x808080_rgb);
g->DrawLine({ 7, 8 }, { 7, 7 + FONT_H * FONT_SCALE }, 0x808080_rgb);
g->DrawLine({ 7 + width * FONT_SCALE, 8}, { 7 + width * FONT_SCALE, 7 + FONT_H * FONT_SCALE }, 0x808080_rgb);
}
}
else
{
g->BlendText({ 8, 8 }, "No character", 0xFF0000_rgb .WithAlpha(255));
}
}
void FontEditor::OnMouseDown(int x, int y, unsigned button)
{
if(fontWidths.find(currentChar) != fontWidths.end())
{
x = (x - 8) / FONT_SCALE;
y = (y - 8) / FONT_SCALE;
if(x >= 0 && y >= 0 && x < fontWidths[currentChar] && y < FONT_H)
{
if(button == SDL_BUTTON_LEFT)
fontPixels[currentChar][y][x] = (fontPixels[currentChar][y][x] + 1) % 4;
else
fontPixels[currentChar][y][x] = (fontPixels[currentChar][y][x] + 3) % 4;
savedButton->SetToggleState(false);
}
}
}
void FontEditor::Translate(std::array<std::array<char, MAX_WIDTH>, FONT_H> &pixels, int dx, int dy)
{
std::array<std::array<char, MAX_WIDTH>, FONT_H> old = pixels;
for(int j = 0; j < FONT_H; j++)
for(int i = 0; i < MAX_WIDTH; i++)
if(i - dx >= 0 && i - dx < MAX_WIDTH && j - dy >= 0 && j - dy < FONT_H)
pixels[j][i] = old[j - dy][i - dx];
else
pixels[j][i] = 0;
savedButton->SetToggleState(false);
}
void FontEditor::OnKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt)
{
if (IsFocused(NULL))
{
switch(scan)
{
case SDL_SCANCODE_UP:
if(shift)
Translate(fontPixels[currentChar], 0, -1);
break;
case SDL_SCANCODE_DOWN:
if(shift)
Translate(fontPixels[currentChar], 0, 1);
break;
case SDL_SCANCODE_LEFT:
if(shift)
Translate(fontPixels[currentChar], -1, 0);
else
PrevChar();
break;
case SDL_SCANCODE_RIGHT:
if(shift)
Translate(fontPixels[currentChar], 1, 0);
else
NextChar();
break;
case SDL_SCANCODE_ESCAPE:
case SDL_SCANCODE_Q:
if(savedButton->GetToggleState())
ui::Engine::Ref().Exit();
else
ui::Engine::Ref().ConfirmExit();
break;
case SDL_SCANCODE_C:
clipboardWidth = fontWidths[currentChar];
clipboardPixels = fontPixels[currentChar];
break;
case SDL_SCANCODE_V:
fontWidths[currentChar] = clipboardWidth;
fontPixels[currentChar] = clipboardPixels;
break;
}
}
}
void FontEditor::UpdateCharNumber()
{
currentCharTextbox->SetText(String::Build(Format::Hex((unsigned int)currentChar)));
}
void FontEditor::PrevChar()
{
if(currentChar > 0)
currentChar--;
UpdateCharNumber();
}
void FontEditor::NextChar()
{
if(currentChar <= 0x10FFFF)
currentChar++;
UpdateCharNumber();
}
void FontEditor::ShrinkChar()
{
if(fontWidths[currentChar] > 0)
fontWidths[currentChar]--;
savedButton->SetToggleState(false);
}
void FontEditor::GrowChar()
{
if(fontWidths[currentChar] < MAX_WIDTH - 1)
fontWidths[currentChar]++;
savedButton->SetToggleState(false);
}
void FontEditor::Render()
{
PackData(fontWidths, fontPixels, fontData, fontPtrs, fontRanges);
font_data = fontData.data();
font_ptrs = fontPtrs.data();
font_ranges = (unsigned int (*)[2])fontRanges.data();
}
void FontEditor::Save()
{
std::vector<unsigned char> tmpFontData;
std::vector<unsigned int> tmpFontPtrs;
std::vector<std::array<unsigned int, 2> > tmpFontRanges;
PackData(fontWidths, fontPixels, tmpFontData, tmpFontPtrs, tmpFontRanges);
WriteDataFile(dataFile, tmpFontData, tmpFontPtrs, tmpFontRanges);
savedButton->SetToggleState(true);
}