diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..7af7d83 --- /dev/null +++ b/.clang-format @@ -0,0 +1,26 @@ +Language: Cpp +AccessModifierOffset: -4 +AlignAfterOpenBracket: DontAlign +AllowShortCaseLabelsOnASingleLine: true +AllowShortIfStatementsOnASingleLine: true +AlwaysBreakTemplateDeclarations: Yes +BraceWrapping: + AfterFunction: true +BreakBeforeBraces: Custom +ColumnLimit: 0 +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^<.*\.h>' + Priority: 2 + - Regex: '^<.*' + Priority: 3 + - Regex: '.*' + Priority: 1 +IndentCaseLabels: true +IndentWidth: 4 +KeepEmptyLinesAtTheStartOfBlocks: false +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: All +PointerAlignment: Left +SpaceAfterCStyleCast: true +SpacesBeforeTrailingComments: 2 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5882e64 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +tab_width = 8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d62b5a1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +*.cpp text +*.h text +*.txt text +*.md text +.* text +*.png filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..0a6d098 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,100 @@ +name: Build and Release + +on: + push: + tags: + - 'v[0-9]+.*' + +permissions: + packages: read + contents: write + +jobs: + create_release: + name: Create Release + runs-on: ubuntu-latest + + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + + steps: + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + + release_assets: + name: Release Assets + needs: create_release + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + build_type: [Release] + cpp_compiler: [g++-13, cl] + include: + - os: windows-latest + cpp_compiler: cl + - os: ubuntu-latest + cpp_compiler: g++-13 + exclude: + - os: windows-latest + cpp_compiler: g++-13 + - os: ubuntu-latest + cpp_compiler: cl + + steps: + - uses: actions/checkout@v3 + + - name: Set Reusable Strings + id: strings + shell: bash + run: | + echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" + + - name: Install GCC + if: ${{ matrix.os == 'ubuntu-latest' }} + shell: bash + run: | + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt-get update + sudo apt-get -y install g++-13 + + - name: Configure CMake + run: > + cmake -B ${{ steps.strings.outputs.build-output-dir }} + -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + -S ${{ github.workspace }} + + - name: Build + run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} + + - name: Upload Ubuntu Assets + if: ${{ matrix.os == 'ubuntu-latest' }} + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_name: vtfontmaker + asset_path: ${{ steps.strings.outputs.build-output-dir }}/vtfontmaker + asset_content_type: application/octet-stream + + - name: Upload Windows Assets + if: ${{ matrix.os == 'windows-latest' }} + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_name: vtfontmaker.exe + asset_path: ${{ steps.strings.outputs.build-output-dir }}/Release/vtfontmaker.exe + asset_content_type: application/octet-stream diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88dbff1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vs/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d62ff17 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.15) +project(vtfontmaker) + +set( + MAIN_FILES + "src/main.cpp" + "src/application.cpp" + "src/canvas.cpp" + "src/capabilities.cpp" + "src/charsets.cpp" + "src/coloring.cpp" + "src/common_dialog.cpp" + "src/dialog.cpp" + "src/font.cpp" + "src/glyphs.cpp" + "src/iso2022.cpp" + "src/keyboard.cpp" + "src/macros.cpp" + "src/menu.cpp" + "src/os.cpp" + "src/status.cpp" + "src/vt.cpp" +) + +set( + DOC_FILES + "README.md" + "LICENSE.txt" +) + +if(WIN32) + add_compile_options("$<$:/utf-8>") +endif() + +add_executable(vtfontmaker ${MAIN_FILES}) + +set_target_properties(vtfontmaker PROPERTIES CXX_STANDARD 20 CXX_STANDARD_REQUIRED On) +source_group("Doc Files" FILES ${DOC_FILES}) diff --git a/CMakeSettings.json b/CMakeSettings.json new file mode 100644 index 0000000..01bc921 --- /dev/null +++ b/CMakeSettings.json @@ -0,0 +1,26 @@ +{ + "configurations": [ + { + "name": "x64-Debug", + "generator": "Ninja", + "configurationType": "Debug", + "inheritEnvironments": [ "msvc_x64_x64" ], + "buildRoot": "${projectDir}\\build\\${name}", + "installRoot": "${projectDir}\\build\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "", + "ctestCommandArgs": "" + }, + { + "name": "x64-Release", + "generator": "Ninja", + "configurationType": "RelWithDebInfo", + "inheritEnvironments": [ "msvc_x64_x64" ], + "buildRoot": "${projectDir}\\build\\${name}", + "installRoot": "${projectDir}\\build\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "", + "ctestCommandArgs": "" + } + ] +} \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..c56c192 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 James Holderness + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..95f0b0d --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +VT Font Maker +============= + +![Screenshot](screenshot.png) + +This is a TUI application for editing VT soft fonts, also known as Dynamically +Redefinable Character Sets. It requires a VT525 (or something of comparable +functionality) to run, but it should be capable of editing fonts from most if +not all of the DEC terminals. + + +Quick Start +----------- + +* Use the cursor keys to move +* Hold down `Alt` while moving to select a range +* Press the `Space` bar to toggle a pixel +* Use `F10` to open the menu + + +Download +-------- + +The latest binaries can be found on GitHub at the following url: + +https://github.com/j4james/vtfontmaker/releases/latest + +For Linux download `vtfontmaker`, and for Windows download `vtfontmaker.exe`. + + +Build Instructions +------------------ + +If you want to build this yourself, you'll need [CMake] version 3.15 or later +and a C++ compiler supporting C++20 or later. + +1. Download or clone the source: + `git clone https://github.com/j4james/vtfontmaker.git` + +2. Change into the build directory: + `cd vtfontmaker/build` + +3. Generate the build project: + `cmake -D CMAKE_BUILD_TYPE=Release ..` + +4. Start the build: + `cmake --build . --config Release` + +[CMake]: https://cmake.org/ + + +License +------- + +The VT Font Maker source code and binaries are released under the MIT License. +See the [LICENSE] file for full license details. + +[LICENSE]: LICENSE.txt diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..66c6ac1 --- /dev/null +++ b/screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:196761b4eda39da6106ac0e6bd2d786ab653240cce05e45e047498fef0528012 +size 16112 diff --git a/src/application.cpp b/src/application.cpp new file mode 100644 index 0000000..6bc0b86 --- /dev/null +++ b/src/application.cpp @@ -0,0 +1,411 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "application.h" + +#include "charsets.h" +#include "common_dialog.h" +#include "dialog.h" + +#include + +namespace { + + enum id { + file_new = 0, + file_open, + file_save, + file_save_as, + file_properties, + file_exit, + + edit_undo, + edit_cut, + edit_copy, + edit_paste, + edit_delete, + edit_select_all, + + view_next, + view_prev, + view_next_used, + view_prev_used, + view_double, + view_reverse, + + transform_invert, + transform_flip_h, + transform_flip_v, + + help_view, + help_about + }; + + using namespace std::literals; + + const auto devices = std::vector{L"VT5xx/VT420 (10x16)"s, L"VT382 (12x30)"s, L"VT340 (10x20)"s, L"VT320 (15x12)"s, L"VT2x0 (10x10)"s, L"Non-standard (16x32)"s}; + const auto screen_sizes = std::vector{L"80x24"s, L"132x24"s, L"80x36"s, L"132x36"s, L"80x48"s, L"132x48"s}; + const auto usages = std::vector{L"Text"s, L"Full cell"s}; + const auto buffers = std::vector{L"First empty buffer"s, L"Buffer #1"s, L"Buffer #2"s}; + const auto erase_types = std::vector{L"All of this buffer"s, L"Only the used characters"s, L"All buffers"s}; + const auto c1_types = std::vector{L"7-bit controls"s, L"8-bit controls"s}; + + constexpr auto widths = std::array{10, 12, 10, 15, 10, 16}; + constexpr auto heights = std::array{16, 30, 20, 12, 10, 32}; + constexpr auto screens_values = std::array{0, 2, 11, 12, 21, 22}; + + const auto find_value = [](const auto& values, const auto search) { + const auto match_pos = std::find(values.begin(), values.end(), search); + return match_pos != values.end() ? std::distance(values.begin(), match_pos) : 0; + }; + +} // namespace + +application::application(capabilities& caps, const std::filesystem::path& filepath) + : _caps{caps}, _status{caps}, _canvas{caps, _glyphs, _status} +{ + _init_menu(); + _menu.render(); + _canvas.render(); + _status.render(); + if (!_open(filepath)) + _new(true); +} + +void application::run() +{ + for (auto exit = false; !exit;) { + _menu.enable(id::edit_undo, _canvas.can_undo()); + _menu.enable(id::edit_paste, _canvas.can_paste()); + const auto key_press = keyboard::read(); + const auto selection = _menu.process_key(key_press); + if (selection) { + switch (selection.value()) { + case id::file_new: _new(); break; + case id::file_open: _open(); break; + case id::file_save: _save(); break; + case id::file_save_as: _save_as(); break; + case id::file_properties: _properties(); break; + case id::file_exit: exit = _exit(); break; + + case id::edit_undo: _canvas.undo(); break; + case id::edit_cut: _canvas.cut_selection(); break; + case id::edit_copy: _canvas.copy_selection(); break; + case id::edit_paste: _canvas.paste(); break; + case id::edit_delete: _canvas.delete_selection(); break; + case id::edit_select_all: _canvas.select_all(); break; + + case id::view_next: _canvas.next_char(); break; + case id::view_prev: _canvas.prev_char(); break; + case id::view_next_used: _canvas.next_char(true); break; + case id::view_prev_used: _canvas.prev_char(true); break; + case id::view_double: _canvas.toggle_double_width(); break; + case id::view_reverse: _canvas.toggle_reverse_screen(); break; + + case id::transform_invert: _canvas.invert(); break; + case id::transform_flip_h: _canvas.flip_horizontally(); break; + case id::transform_flip_v: _canvas.flip_vertically(); break; + + case id::help_about: _about(); break; + } + } else { + _canvas.process_key(key_press); + } + } +} + +void application::_init_menu() +{ + auto file_menu = _menu.add(L"&File"); + file_menu.add(id::file_new, L"&New...", key::ctrl + key::n); + file_menu.add(id::file_open, L"&Open...", key::ctrl + key::o); + file_menu.add(id::file_save, L"&Save", key::ctrl + key::s); + file_menu.add(id::file_save_as, L"Save &As..."); + file_menu.separator(); + file_menu.add(id::file_properties, L"&Properties"); + file_menu.separator(); + file_menu.add(id::file_exit, L"E&xit"); + auto edit_menu = _menu.add(L"&Edit"); + edit_menu.add(id::edit_undo, L"&Undo", key::ctrl + key::z); + edit_menu.separator(); + edit_menu.add(id::edit_cut, L"Cu&t", key::ctrl + key::x, key::shift + key::del); + edit_menu.add(id::edit_copy, L"&Copy", key::ctrl + key::c, key::ctrl + key::ins); + edit_menu.add(id::edit_paste, L"&Paste", key::ctrl + key::v, key::shift + key::ins); + edit_menu.add(id::edit_delete, L"De&lete", key::del); + edit_menu.separator(); + edit_menu.add(id::edit_select_all, L"Select &All", key::ctrl + key::a); + auto view_menu = _menu.add(L"&View"); + view_menu.add(id::view_next, L"&Next Glyph", key::pgdn); + view_menu.add(id::view_prev, L"&Previous Glyph", key::pgup); + view_menu.add(id::view_next_used, L"Next &Used Glyph", key::ctrl + key::pgdn); + view_menu.add(id::view_prev_used, L"Previous U&sed Glyph", key::ctrl + key::pgup); + view_menu.separator(); + view_menu.add(id::view_double, L"&Double Width"); + view_menu.add(id::view_reverse, L"&Reverse Video"); + auto transform_menu = _menu.add(L"&Transform"); + transform_menu.add(id::transform_invert, L"&Invert Pixels"); + transform_menu.add(id::transform_flip_h, L"Flip &Horizontally"); + transform_menu.add(id::transform_flip_v, L"Flip &Vertically"); + auto help_menu = _menu.add(L"&Help"); + help_menu.add(id::help_view, L"&View Help", key::pf1, key::help); + help_menu.separator(); + help_menu.add(id::help_about, std::format(L"&About {}", wname)); +} + +bool application::_can_clear() +{ + _canvas.flush(); + if (!_status.dirty()) + return true; + else { + const auto filename = _status.filename(); + const auto message = L"Do you want to save changes to " + filename + L"?"; + using id = common_dialog::id; + switch (common_dialog::message_box(wname, message, id::yes | id::no | id::cancel)) { + case id::yes: return _save(); + case id::no: return true; + default: return false; + } + } +} + +bool application::_clear(const bool use_defaults) +{ + if (use_defaults) { + _glyphs.clear(); + return true; + } + + auto dlg = dialog{L"New"}; + auto& device_field = dlg.add_dropdown(L"Target device", devices); + auto& screen_size_field = dlg.add_dropdown(L"Target screen", screen_sizes); + auto& usage_field = dlg.add_dropdown(L"Font usage", usages); + auto& charset_field = dlg.add_dropdown(L"Character set", charset::names()); + auto& buttons = dlg.add_group(dialog::alignment::right); + buttons.add_button(L"OK", 1, true); + buttons.add_button(L"Cancel", 2); + + device_field.on_change([&] { + const auto device = device_field.selection(); + const auto screen = screen_size_field.selection(); + // VT5xx/VT420 supports all screen size, the "custom" device only + // supports 80x24, and everything else support 80x24 and 132x24. + if (device == 0) + screen_size_field.options(screen_sizes); + else if (device == 5) + screen_size_field.options({L"80x24"}); + else + screen_size_field.options({L"80x24", L"132x24"}); + screen_size_field.selection(screen); + }); + screen_size_field.on_change([&] { + const auto device = device_field.selection(); + const auto screen = screen_size_field.selection(); + const auto usage = usage_field.selection(); + // VT2x0 only supports text usage at 80x24. + if (device == 4 && screen == 0) + usage_field.options({L"Text"}); + else + usage_field.options(usages); + // VT2x0 only supports 94-glyph character sets. + if (device == 4) + charset_field.options(charset::names_for_size(94)); + else + charset_field.options(charset::names()); + usage_field.selection(usage); + }); + + const auto& current = _glyphs.params(); + device_field.selection(current.pss() <= 2 ? find_value(heights, _glyphs.cell_height()) : 0); + screen_size_field.selection(find_value(screens_values, current.pss())); + usage_field.selection(current.pu() == 2 ? 1 : 0); + + if (dlg.show() == 2) return false; + + const auto device = device_field.selection(); + auto pcmw = widths[device]; + auto pcmh = heights[device]; + + const auto size = screen_size_field.selection(); + if (size % 2 == 1) pcmw = pcmw * 80 / 132; + if (size / 2 == 1) pcmh = 10; + if (size / 2 == 2) pcmh = 8; + const auto usage = usage_field.selection(); + if (usage == 0) pcmw = (pcmw * 8 + 5) / 10; + const auto pcms = pcmw >> 1; + + const auto cs_index = charset_field.selection(); + const auto cs = device == 4 ? charset::from_index(cs_index, 94) : charset::from_index(cs_index); + + auto params = std::vector{}; + params.push_back(0); // pfn + params.push_back(0); // pcn + params.push_back(0); // pe + params.push_back(device == 4 ? pcms : pcmw); + params.push_back(screens_values[size]); // pss + params.push_back(usage ? 2 : 0); // pu + if (device != 4) { + const auto pcss = cs->size() == 96 ? 1 : 0; + params.push_back(pcmh); + params.push_back(pcss); + } + + _glyphs.clear(params, cs->id()); + return true; +} + +void application::_new(const bool use_defaults) +{ + if (_can_clear() && _clear(use_defaults)) { + _filepath.clear(); + _status.filename(L"Untitled"); + _status.character_set(_glyphs.id(), _glyphs.size()); + _status.dirty(false); + _canvas.refresh(); + } +} + +bool application::_save() +{ + if (_filepath.empty()) + return _save_as(); + else { + _canvas.flush(); + const auto success = _glyphs.save(_filepath); + if (success) _status.dirty(false); + return success; + } +} + +bool application::_save_as() +{ + _canvas.flush(); + auto filepath = _filepath; + if (filepath.empty()) { + filepath = std::filesystem::current_path(); + filepath.append(L"Untitled.fnt"); + } + const auto new_filepath = common_dialog::save(filepath); + if (new_filepath.empty()) + return false; + else { + const auto success = _glyphs.save(new_filepath); + if (success) { + _filepath = new_filepath; + _status.filename(_filepath.filename().wstring()); + _status.dirty(false); + } + return success; + } +} + +bool application::_open() +{ + if (_can_clear()) + return _open(common_dialog::open()); + else + return false; +} + +bool application::_open(const std::filesystem::path& filepath) +{ + using id = common_dialog::id; + if (filepath.empty()) + return false; + const auto filename = filepath.filename().wstring(); + if (!std::filesystem::exists(filepath)) { + const auto message = filename + L"\nFile not found."; + common_dialog::message_box(wname, message, id::ok); + return false; + } + if (!_glyphs.load(filepath)) { + const auto message = filename + L"\nThis is not a valid font file or its format\nis not currently supported."; + common_dialog::message_box(wname, message, id::ok); + return false; + } + _filepath = filepath; + _status.filename(filepath.filename().wstring()); + _status.character_set(_glyphs.id(), _glyphs.size()); + _status.dirty(false); + _canvas.refresh(); + return true; +} + +void application::_properties() +{ + auto charsets = charset::names_for_size(_glyphs.size()); + auto charset_index = charset::index_of(_glyphs.id(), _glyphs.size()).value_or(-1); + if (charset_index < 0) { + const auto id = std::wstring{_glyphs.id().begin(), _glyphs.id().end()}; + charsets.push_back(std::format(L"Other: {}", id)); + charset_index = charsets.size() - 1; + } + const auto& current = _glyphs.params(); + const auto buffer_index = current.pfn().value_or(0); + const auto erase_index = current.pe().value_or(0); + const auto c1_type = _glyphs.c1_controls(); + const auto c1_index = c1_type ? int(c1_type.value()) : -1; + + auto dlg = dialog{L"Properties"}; + auto& charset_field = dlg.add_dropdown(L"Character set", charsets); + auto& buffer_field = dlg.add_dropdown(L"Target buffer", buffers); + auto& erase_field = dlg.add_dropdown(L"Erased range", erase_types); + auto* c1_field = c1_type.has_value() ? &dlg.add_dropdown(L"Sequence format", c1_types) : nullptr; + auto& buttons = dlg.add_group(dialog::alignment::right); + buttons.add_button(L"Save", 1, true); + buttons.add_button(L"Cancel", 2); + + charset_field.selection(charset_index); + buffer_field.selection(buffer_index); + erase_field.selection(erase_index); + if (c1_field) c1_field->selection(c1_index); + + if (dlg.show() == 1) { + if (charset_field.selection() != charset_index) { + const auto cs = charset::from_index(charset_field.selection(), _glyphs.size()); + if (cs) { + _glyphs.id(cs->id()); + const auto status_index = _status.index(); + _status.character_set(_glyphs.id(), _glyphs.size()); + _status.index(status_index); + _status.dirty(true); + } + } + if (buffer_field.selection() != buffer_index) { + _glyphs.params().pfn(buffer_field.selection()); + _status.dirty(true); + } + if (erase_field.selection() != erase_index) { + _glyphs.params().pe(erase_field.selection()); + _status.dirty(true); + } + if (c1_field && c1_field->selection() != c1_index) { + _glyphs.c1_controls(c1_field->selection()); + _status.dirty(true); + } + } +} + +bool application::_exit() +{ + return _can_clear(); +} + +void application::_about() +{ + auto dlg = dialog{L"About"}; + auto& group = dlg.add_group(); + auto& left = group.add_group(); + auto& right = group.add_group(); + left.add_text(L" \uE041\uE042\uE043"); + left.add_text(L"\uE044\uE045\uE046 "); + right.add_text(wname); + right.add_text(std::format(L"Version {}.{}.{}", major_version, minor_version, patch_number)); + dlg.add_gap(); + dlg.add_text(L"©2024 James Holderness"); + dlg.add_text(L"All Rights Reserved"); + auto& buttons = dlg.add_group(dialog::alignment::right); + buttons.add_button(L"OK", 0, true); + dlg.show(); +} diff --git a/src/application.h b/src/application.h new file mode 100644 index 0000000..a173566 --- /dev/null +++ b/src/application.h @@ -0,0 +1,46 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include "canvas.h" +#include "glyphs.h" +#include "menu.h" +#include "status.h" + +#include + +class capabilities; + +class application { +public: + static constexpr auto name = "VT Font Maker"; + static constexpr auto wname = L"VT Font Maker"; + static constexpr auto major_version = 1; + static constexpr auto minor_version = 0; + static constexpr auto patch_number = 0; + + application(capabilities& caps, const std::filesystem::path& filepath); + void run(); + +private: + void _init_menu(); + bool _can_clear(); + bool _clear(const bool use_defaults); + void _new(const bool use_defaults = false); + bool _save(); + bool _save_as(); + bool _open(); + bool _open(const std::filesystem::path& filepath); + void _properties(); + bool _exit(); + void _about(); + + const capabilities& _caps; + menu _menu; + status _status; + glyph_manager _glyphs; + canvas _canvas; + std::filesystem::path _filepath; +}; diff --git a/src/canvas.cpp b/src/canvas.cpp new file mode 100644 index 0000000..772daaf --- /dev/null +++ b/src/canvas.cpp @@ -0,0 +1,505 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "canvas.h" + +#include "capabilities.h" +#include "glyphs.h" +#include "macros.h" +#include "status.h" +#include "vt.h" + +namespace color { + + constexpr auto wallpaper = {0, 37, 45}; // White on LighterBlue + + constexpr auto dark_grid_init = {0, 31, 40}; // DarkGray on Black + constexpr auto dark_grid_alt_init = {30, 41}; // Black on DarkGray + constexpr int dark_grid[] = {0, 1}; // Black, DarkGray + constexpr int dark_grid_focus = 2; // DarkerBlue, DarkBlue + constexpr int dark_pixel = 7; // White + constexpr int dark_pixel_focus = 5; // LighterBlue + + constexpr auto light_grid_init = {0, 36, 47}; // LightGray on White + constexpr auto light_grid_alt_init = {37, 46}; // White on LightGray + constexpr int light_grid[] = {7, 6}; // White, LightGray + constexpr int light_grid_focus = -2; // LighterBlue, LightBlue + constexpr int light_pixel = 0; // Black + constexpr int light_pixel_focus = 2; // DarkerBlue + +} // namespace color + +canvas::canvas(const capabilities& caps, glyph_manager& glyphs, status& status) + : _caps{caps}, _glyphs{glyphs}, _status{status} +{ + _grid_macro = macro_manager::reserve_id(); + _wallpaper_macro = macro_manager::create([&](auto& macro) { + macro.ls1(); + macro.sgr(color::wallpaper); + macro.decfra('@', 2, {}, caps.height - 1, {}); + macro.ls0(); + }); +} + +void canvas::render() +{ + vtout.decinvm(_wallpaper_macro); + _need_wallpaper = false; +} + +void canvas::refresh() +{ + _focus = {}; + _selection = {}; + _cell_height = _glyphs.cell_height(); + _cell_width = _glyphs.cell_width(); + _pixel_ar_unscaled = _glyphs.pixel_aspect_ratio(); + _calculate_dimensions(); + _pixels.clear(); + _char_index = {}; + _load_char(_glyphs.first_used(), 0); +} + +void canvas::select_all() +{ + _select_range({}, {_cell_height - 1, _cell_width - 1}); +} + +void canvas::cut_selection() +{ + copy_selection(); + delete_selection(); +} + +void canvas::copy_selection() +{ + _clipboard.clear(); + _for_each_selection([&](const auto pos, auto& pixel) { + _clipboard.push_back(pixel); + }); + const auto copied_range = _make_range(); + _clipboard_size = copied_range.extent(); + _select_range(copied_range.origin(), copied_range.extent()); +} + +void canvas::delete_selection() +{ + _fill_selection(0); +} + +bool canvas::_range_contains(const range& r, const coord pos) +{ + return pos.x >= r.x.first && pos.x <= r.x.second && pos.y >= r.y.first && pos.y <= r.y.second; +} + +void canvas::paste() +{ + if (can_paste()) { + _save_history(); + const auto focused_range = _make_range(_focus, _selection); + const auto origin = focused_range.origin(); + for (auto y = origin.y, i = 0; y <= origin.y + _clipboard_size.h; y++) { + for (auto x = origin.x; x <= origin.x + _clipboard_size.w; x++) { + const auto is_point = _clipboard[i++]; + if (is_point && y < _cell_height && x < _cell_width) { + _pixel({y, x}) = 1; + if (_range_contains(focused_range, {y, x})) + _render_pixel({y, x}, true, true); + } + } + } + _select_range(origin, _clipboard_size); + } +} + +void canvas::undo() +{ + if (can_undo()) { + const auto entry_size = 4 + _cell_width * _cell_height; + auto offset = _history.size() - entry_size; + const auto focus = coord{_history[offset++], _history[offset++]}; + const auto selection = size{_history[offset++], _history[offset++]}; + _select_range(focus, selection); + const auto focused_range = _make_range(_focus, _selection); + for (auto y = 0; y < _cell_height; y++) + for (auto x = 0; x < _cell_width; x++) { + auto new_pixel = _history[offset++]; + auto& pixel = _pixel({y, x}); + if (pixel != new_pixel) { + pixel = new_pixel; + _render_pixel({y, x}, new_pixel, focused_range); + } + } + _history.resize(_history.size() - entry_size); + _history.shrink_to_fit(); + } +} + +bool canvas::can_paste() const +{ + return !_clipboard.empty(); +} + +bool canvas::can_undo() const +{ + const auto entry_size = 4 + _cell_width * _cell_height; + return _history.size() >= entry_size; +} + +void canvas::invert() +{ + _save_history(); + const auto focused_range = _make_range(_focus, _selection); + _for_each_selection([&](const auto pos, auto& pixel) { + pixel ^= 1; + _render_pixel(pos, pixel, focused_range); + }); +} + +void canvas::flip_horizontally() +{ + _save_history(); + const auto focused_range = _make_range(_focus, _selection); + const auto target_range = _make_range(); + const auto origin = target_range.origin(); + const auto extent = target_range.extent(); + for (auto y = 0; y <= extent.h; y++) { + for (auto x = 0; x < (extent.w + 1) / 2; x++) { + const auto pos1 = coord{origin.y + y, origin.x + x}; + const auto pos2 = coord{origin.y + y, origin.x + extent.w - x}; + auto& pixel1 = _pixel(pos1); + auto& pixel2 = _pixel(pos2); + if (pixel1 != pixel2) { + std::swap(pixel1, pixel2); + _render_pixel(pos1, pixel1, focused_range); + _render_pixel(pos2, pixel2, focused_range); + } + } + } +} + +void canvas::flip_vertically() +{ + _save_history(); + const auto focused_range = _make_range(_focus, _selection); + const auto target_range = _make_range(); + const auto origin = target_range.origin(); + const auto extent = target_range.extent(); + for (auto y = 0; y < (extent.h + 1) / 2; y++) { + for (auto x = 0; x <= extent.w; x++) { + const auto pos1 = coord{origin.y + y, origin.x + x}; + const auto pos2 = coord{origin.y + extent.h - y, origin.x + x}; + auto& pixel1 = _pixel(pos1); + auto& pixel2 = _pixel(pos2); + if (pixel1 != pixel2) { + std::swap(pixel1, pixel2); + _render_pixel(pos1, pixel1, focused_range); + _render_pixel(pos2, pixel2, focused_range); + } + } + } +} + +void canvas::next_char(const bool only_used) +{ + _load_char(_char_index.value_or(-1), +1, only_used); +} + +void canvas::prev_char(const bool only_used) +{ + _load_char(_char_index.value_or(100), -1, only_used); +} + +void canvas::toggle_double_width() +{ + _double_width = !_double_width; + _calculate_dimensions(); + _render(); +} + +void canvas::toggle_reverse_screen() +{ + _reversed = !_reversed; + _grid_macro_initialized = false; + _render(); +} + +void canvas::process_key(const key key_press) +{ + switch (key_press) { + case key::home: + _load_char(0, 0); + break; + case key::end: + _load_char(100, 0); + break; + case key::up: + _select_range({_focus.y - 1, _focus.x}); + break; + case key::down: + _select_range({_focus.y + 1, _focus.x}); + break; + case key::left: + _select_range({_focus.y, _focus.x - 1}); + break; + case key::right: + _select_range({_focus.y, _focus.x + 1}); + break; + case key::alt + key::up: + _select_range(_focus, {_selection.h - 1, _selection.w}); + break; + case key::alt + key::down: + _select_range(_focus, {_selection.h + 1, _selection.w}); + break; + case key::alt + key::left: + _select_range(_focus, {_selection.h, _selection.w - 1}); + break; + case key::alt + key::right: + _select_range(_focus, {_selection.h, _selection.w + 1}); + break; + case key::space: + if (_selection == size{}) + _toggle_pixel(_focus); + else + _fill_selection(1); + break; + } +} + +void canvas::flush() +{ + if (_char_index && _dirty) { + _glyphs[_char_index.value()] = _pixels; + _dirty = false; + } +} + +void canvas::_render() +{ + _render_grid(); + const auto focused_range = _make_range(_focus, _selection); + for (auto y = 0; y < _cell_height; y++) { + for (auto x = 0; x < _cell_width; x++) { + const bool focused = _range_contains(focused_range, {y, x}); + if (_pixel({y, x})) { + auto x2 = x + 1; + while (x2 < _cell_width && _pixel({y, x2}) && focused == _range_contains(focused_range, {y, x2})) + x2++; + _render_pixel_run({y, x}, x2 - x, true, focused); + x = x2 - 1; + } else if (focused) { + _render_pixel({y, x}, false, true); + } + } + } +} + +void canvas::_render_grid() +{ + if (!_grid_macro_initialized) { + _grid_macro_initialized = true; + + static int grid_macro_inner = macro_manager::reserve_id(); + macro_manager::create(grid_macro_inner, [&](auto& macro) { + const auto color_grid_alt_init = _reversed ? color::light_grid_alt_init : color::dark_grid_alt_init; + const auto pattern_height = _pixel_pattern.length(); + const auto reps = (_cell_width + 1) / 2; + macro.decstbm(_top, _top + _render_height - 1); + macro.il(pattern_height); + macro.decstbm(_top, _top + pattern_height - 1); + macro.repeat(reps, [&](auto& macro2) { + macro2.decic(_pixel_width * 2); + for (auto i = 0; i < pattern_height; i++) + macro2.decfra(_pixel_pattern[i], i + 1, {}, i + 1, _pixel_width * 2); + macro2.deccara({}, {}, pattern_height, _pixel_width, color_grid_alt_init); + }); + }); + + macro_manager::create(_grid_macro, [&](auto& macro) { + const auto color_grid_init = _reversed ? color::light_grid_init : color::dark_grid_init; + const auto pattern_height = _pixel_pattern.length(); + const auto reps = (_render_height + pattern_height - 1) / pattern_height; + macro.sgr(color_grid_init); + macro.ls1(); + macro.decslrm(_left, _left + _render_width - 1); + macro.repeat(reps, [&](auto& macro2) { + macro2.decinvm(grid_macro_inner); + }); + macro.decstbm(); + macro.decslrm(); + macro.ls0(); + }); + } + if (_need_wallpaper) render(); + vtout.decinvm(_grid_macro); +} + +void canvas::_load_char(const int start_index, const int increment, const bool only_used) +{ + const auto size = _glyphs.size(); + const auto min_index = size == 96 ? 0 : 1; + const auto max_index = size == 96 ? 95 : 94; + auto index = start_index; + do { + index = std::clamp(index + increment, min_index, max_index); + if (index == min_index || index == max_index) break; + } while (only_used && !_glyphs[index].used()); + if (index != _char_index) { + flush(); + _clear_history(); + _pixels = _glyphs[index]; + _char_index = index; + _render(); + _status.index(_char_index.value()); + } +} + +void canvas::_select_range(const coord origin, const size extent) +{ + const auto new_y = std::clamp(origin.y, 0, _cell_height - 1); + const auto new_x = std::clamp(origin.x, 0, _cell_width - 1); + const auto new_h = std::clamp(extent.h, -new_y, _cell_height - new_y - 1); + const auto new_w = std::clamp(extent.w, -new_x, _cell_width - new_x - 1); + if (_focus != coord{new_y, new_x} || _selection != size{new_h, new_w}) { + const auto new_range = _make_range({new_y, new_x}, {new_h, new_w}); + const auto old_range = _make_range(_focus, _selection); + for (auto y = 0; y < _cell_height; y++) { + const auto y_inside_old = y >= old_range.y.first && y <= old_range.y.second; + const auto y_inside_new = y >= new_range.y.first && y <= new_range.y.second; + for (auto x = 0; x < _cell_width; x++) { + const auto inside_old = x >= old_range.x.first && x <= old_range.x.second && y_inside_old; + const auto inside_new = x >= new_range.x.first && x <= new_range.x.second && y_inside_new; + if (inside_old != inside_new) + _render_pixel({y, x}, _pixel({y, x}), inside_new); + } + } + _focus = {new_y, new_x}; + _selection = {new_h, new_w}; + } +} + +canvas::range canvas::_make_range() const +{ + if (_selection == size{}) + return _make_range({}, {_cell_height - 1, _cell_width - 1}); + else + return _make_range(_focus, _selection); +} + +canvas::range canvas::_make_range(const coord origin, const size extent) +{ + auto y1 = origin.y; + auto y2 = origin.y + extent.h; + if (y1 > y2) std::swap(y1, y2); + auto x1 = origin.x; + auto x2 = origin.x + extent.w; + if (x1 > x2) std::swap(x1, x2); + return {{y1, y2}, {x1, x2}}; +} + +void canvas::_render_pixel(const coord pos, const bool set, const range& focused_range) +{ + _render_pixel(pos, set, _range_contains(focused_range, pos)); +} + +void canvas::_render_pixel(const coord pos, const bool set, const bool focused) +{ + _render_pixel_run(pos, 1, set, focused); +} + +void canvas::_render_pixel_run(const coord pos, const int length, const bool set, const bool focused) +{ + const auto top = pos.y * _pixel_ar / 100 + _top; + const auto bottom = ((pos.y + 1) * _pixel_ar - 1) / 100 + _top; + const auto left = pos.x * _pixel_width + _left; + const auto right = left + (_pixel_width * length) - 1; + + const auto color_pixel = _reversed ? color::light_pixel : color::dark_pixel; + const auto color_pixel_focus = _reversed ? color::light_pixel_focus : color::dark_pixel_focus; + const auto color_grid = _reversed ? color::light_grid : color::dark_grid; + const auto color_grid_focus = _reversed ? color::light_grid_focus : color::dark_grid_focus; + const auto fg_color = focused ? color_pixel_focus : color_pixel; + const auto bg_color = color_grid[(pos.x + pos.y) % 2] + (focused ? color_grid_focus : 0); + + auto attr = set ? fg_color : bg_color; + attr += (pos.y % 2 ? 40 : 30); + vtout.deccara(top, left, bottom, right, {attr}); +} + +void canvas::_toggle_pixel(const coord pos) +{ + _save_history(); + auto& pixel = _pixel(pos); + pixel ^= 1; + _render_pixel(pos, pixel, true); +} + +void canvas::_fill_selection(const int fill) +{ + _save_history(); + const auto focused_range = _make_range(_focus, _selection); + _for_each_selection([&](const auto pos, auto& pixel) { + if (pixel != fill) { + pixel = fill; + _render_pixel(pos, fill, focused_range); + } + }); +} + +int8_t& canvas::_pixel(const coord pos) +{ + return _pixels[pos.y * _cell_width + pos.x]; +} + +void canvas::_calculate_dimensions() +{ + const auto free_height = (_caps.height - 4) * 100; + const auto scale_down = _cell_height * _pixel_ar_unscaled > free_height; + _pixel_ar = scale_down ? _pixel_ar_unscaled >> 1 : _pixel_ar_unscaled; + if (_pixel_ar >= 250) { + _pixel_pattern = R"(##^ )"; + _pixel_ar = 250; + } else if (_pixel_ar >= 200) { + _pixel_pattern = R"(## )"; + _pixel_ar = 200; + } else if (_pixel_ar >= 125) { + _pixel_pattern = R"(#"_+ )"; + _pixel_ar = 125; + } else if (_pixel_ar >= 100) { + _pixel_pattern = R"(# )"; + _pixel_ar = 100; + } else if (_pixel_ar >= 80) { + _pixel_pattern = R"(82641735)"; + _pixel_ar = 80; + } else { + _pixel_pattern = R"(^^)"; + _pixel_ar = 50; + } + _pixel_height = (_pixel_ar + 99) / 100; + _pixel_width = (_double_width ? 2 : 1) * (scale_down ? 1 : 2); + _render_height = (_cell_height * _pixel_ar + 99) / 100; + _render_width = _cell_width * _pixel_width; + _top = std::max((_caps.height - _render_height) / 2 + 1, 1); + _left = std::max((_caps.width - _render_width) / 2 + 1, 1); + _grid_macro_initialized = false; + _need_wallpaper = true; +} + +void canvas::_save_history() +{ + _dirty = true; + _status.dirty(true); + _history.push_back(_focus.y); + _history.push_back(_focus.x); + _history.push_back(_selection.h); + _history.push_back(_selection.w); + _history.insert(_history.end(), _pixels.begin(), _pixels.end()); +} + +void canvas::_clear_history() +{ + _dirty = false; + _history.clear(); + _history.shrink_to_fit(); +} diff --git a/src/canvas.h b/src/canvas.h new file mode 100644 index 0000000..6de82f6 --- /dev/null +++ b/src/canvas.h @@ -0,0 +1,113 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include "keyboard.h" + +#include +#include +#include +#include + +class capabilities; +class glyph_manager; +class status; + +class canvas { +public: + canvas(const capabilities& caps, glyph_manager& glyphs, status& status); + void render(); + void refresh(); + void select_all(); + void cut_selection(); + void copy_selection(); + void delete_selection(); + void paste(); + void undo(); + bool can_paste() const; + bool can_undo() const; + void invert(); + void flip_horizontally(); + void flip_vertically(); + void next_char(const bool only_used = false); + void prev_char(const bool only_used = false); + void toggle_double_width(); + void toggle_reverse_screen(); + void process_key(const key key_press); + void flush(); + +private: + struct coord { + int y = 0; + int x = 0; + auto operator<=>(const coord&) const = default; + }; + struct size { + int h = 0; + int w = 0; + auto operator<=>(const size&) const = default; + }; + struct range { + std::pair y; + std::pair x; + coord origin() const { return {y.first, x.first}; } + size extent() const { return {y.second - y.first, x.second - x.first}; } + }; + + void _render(); + void _render_grid(); + void _load_char(const int start_index, const int increment, const bool only_used = false); + void _select_range(const coord origin, const size extent = {0, 0}); + range _make_range() const; + static range _make_range(const coord origin, const size extent); + static bool _range_contains(const range& r, const coord pos); + void _render_pixel(const coord pos, const bool set, const range& focused_range); + void _render_pixel(const coord pos, const bool set, const bool focused); + void _render_pixel_run(const coord pos, const int length, const bool set, const bool focused); + void _toggle_pixel(const coord pos); + void _fill_selection(const int fill); + int8_t& _pixel(const coord pos); + void _calculate_dimensions(); + void _save_history(); + void _clear_history(); + + template + void _for_each_selection(T&& lambda) + { + const auto r = _make_range(); + for (auto yindex = r.y.first; yindex <= r.y.second; yindex++) + for (auto xindex = r.x.first; xindex <= r.x.second; xindex++) + lambda(coord{yindex, xindex}, _pixel({yindex, xindex})); + } + + const capabilities& _caps; + glyph_manager& _glyphs; + status& _status; + bool _grid_macro_initialized = false; + int _grid_macro; + int _wallpaper_macro; + int _cell_height = 16; + int _cell_width = 10; + int _render_height = 20; + int _render_width = 20; + int _pixel_height = 2; + int _pixel_width = 2; + int _pixel_ar = 125; + int _pixel_ar_unscaled = 125; + std::string _pixel_pattern; + int _top = 1; + int _left = 1; + bool _double_width = false; + bool _reversed = false; + bool _need_wallpaper = true; + coord _focus; + size _selection; + std::vector _pixels; + std::vector _history; + std::vector _clipboard; + size _clipboard_size; + std::optional _char_index; + bool _dirty = false; +}; diff --git a/src/capabilities.cpp b/src/capabilities.cpp new file mode 100644 index 0000000..dc885df --- /dev/null +++ b/src/capabilities.cpp @@ -0,0 +1,171 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "capabilities.h" + +#include "os.h" +#include "vt.h" + +#include + +using namespace std::string_literals; + +capabilities::capabilities() +{ + // Save the cursor position. + vtout.decsc(); + // Request 7-bit C1 controls from the terminal. + vtout.s7c1t(); + // Determine the screen size. + vtout.cup(999, 999); + vtout.dsr(6); + const auto size = _query(R"(\x1B\[(\d+);(\d+)R)", false); + if (!size.empty()) { + height = std::stoi(size[1]); + width = std::stoi(size[2]); + } + // Retrieve the device attributes report. + _query_device_attributes(); + // Retrieve the keyboard type. + _query_keyboard_type(); + // Disable scrollback and page coupling. + _original_decrpl = query_mode(112); + _original_decpccm = query_mode(64); + vtout.rm('?', {112, 64}); + // Try and move to page 3 and check the result with DECXCPR. + vtout.ppa(3); + vtout.dsr('?', 6); + const auto page = _query(R"(\x1B\[\??\d+;\d+(?:;(\d+))?R)", true); + if (!page.empty() && page[1].matched) + has_pages = std::stoi(page[1]) == 3; + // Restore the cursor position. + vtout.decrc(); + // Make sure we've returned to page 1. + vtout.ppa(1); + vtout.sm('?', 64); + vtout.rm('?', 64); +} + +capabilities::~capabilities() +{ + // Restore the original DECPCCM and DECRPL modes. + if (_original_decpccm == true) + vtout.sm('?', 64); + else if (_original_decpccm == false) + vtout.rm('?', 64); + if (_original_decrpl == true) + vtout.sm('?', 112); + else if (_original_decrpl == false) + vtout.rm('?', 112); +} + +std::optional capabilities::query_mode(const int mode) const +{ + vtout.decrqm('?', mode); + const auto report = _query(R"(\x1B\[\?(\d+);(\d+)\$y)", true); + if (!report.empty()) { + const auto returned_mode = std::stoi(report[1]); + const auto status = std::stoi(report[2]); + if (returned_mode == mode) { + if (status == 1) return true; + if (status == 2) return false; + } + } + return {}; +} + +std::string capabilities::query_color_table() const +{ + vtout.decctr(2); + const auto report = _query(R"(\x1BP2\$s(.*)\x1B\\)", true); + if (!report.empty()) + return report[1]; + else + return {}; +} + +void capabilities::_query_keyboard_type() +{ + vtout.dsr('?', 26); + const auto report = _query(R"(\x1B\[\?27;\d+;\d+;(\d+)n)", true); + if (!report.empty()) { + // Likely a PC layout if type is LK443 (2) or PCXAL (5). + const auto type = std::stoi(report[1]); + has_pc_keyboard = type == 2 || type == 5; + } else { + // If no type found, likely an older terminal with LK201. + has_pc_keyboard = false; + } +} + +void capabilities::_query_device_attributes() +{ + vtout.da(); + // The Reflection Desktop terminal sometimes uses comma separators + // instead of semicolons in their DA report, so we allow for either. + const auto report = _query(R"(\x1B\[\?(\d+)([;,\d]*)c)", false); + if (!report.empty()) { + // The first parameter indicates the terminal conformance level. + const auto level = std::stoi(report[1]); + // Level 4+ conformance implies support for features 28 and 32. + if (level >= 64) { + has_rectangle_ops = true; + has_macros = true; + } + // The remaining parameters indicate additional feature extensions. + const auto features = report[2].str(); + const auto digits = std::regex(R"(\d+)"); + auto it = std::sregex_iterator(features.begin(), features.end(), digits); + while (it != std::sregex_iterator()) { + const auto feature = std::stoi(it->str()); + switch (feature) { + case 7: has_soft_fonts = true; break; + case 21: has_horizontal_scrolling = true; break; + case 22: has_color = true; break; + case 28: has_rectangle_ops = true; break; + case 32: has_macros = true; break; + } + it++; + } + } +} + +std::smatch capabilities::_query(const char* pattern, const bool may_not_work) +{ + auto final_char = pattern[strlen(pattern) - 1]; + if (may_not_work) { + // If we're uncertain this query is supported, we'll send an extra DA + // or DSR-CPR query to make sure that we get some kind of response. + if (final_char == 'R') { + final_char = 'c'; + vtout.da(); + } else { + final_char = 'R'; + vtout.dsr(6); + } + } + vtout.flush(); + // This needs to be static so the returned smatch can reference it. + static auto response = std::string{}; + response.clear(); + auto last_escape = 0; + for (;;) { + const auto ch = os::getch(); + // Ignore XON, XOFF + if (ch == '\021' || ch == '\023') + continue; + // If we've sent an extra query, the last escape should be the + // start of that response, which we'll ultimately drop. + if (may_not_work && ch == '\033') + last_escape = response.length(); + response += ch; + if (ch == final_char) break; + } + // Drop the extra response if one was requested. + if (may_not_work) + response = response.substr(0, last_escape); + auto match = std::smatch{}; + std::regex_match(response, match, std::regex(pattern)); + return match; +} diff --git a/src/capabilities.h b/src/capabilities.h new file mode 100644 index 0000000..17d2c4d --- /dev/null +++ b/src/capabilities.h @@ -0,0 +1,35 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include +#include +#include + +class capabilities { +public: + capabilities(); + ~capabilities(); + std::optional query_mode(const int mode) const; + std::string query_color_table() const; + + int width = 80; + int height = 24; + bool has_soft_fonts = false; + bool has_horizontal_scrolling = false; + bool has_color = false; + bool has_rectangle_ops = false; + bool has_macros = false; + bool has_pages = false; + bool has_pc_keyboard = false; + +private: + void _query_device_attributes(); + void _query_keyboard_type(); + static std::smatch _query(const char* pattern, const bool may_not_work); + + std::optional _original_decrpl; + std::optional _original_decpccm; +}; diff --git a/src/charsets.cpp b/src/charsets.cpp new file mode 100644 index 0000000..acaf212 --- /dev/null +++ b/src/charsets.cpp @@ -0,0 +1,105 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "charsets.h" + +const std::array charset::all = {{ + + {L"Unregistered/94", " @", 94, L""}, + {L"Unregistered/96", " @", 96, L""}, + {L"ASCII", "B", 94, L"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"}, + {L"Latin-1 (ISO)", "A", 96, L" ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ"}, + {L"Latin-2 (ISO)", "B", 96, L" Ą˘Ł¤ĽŚ§¨ŠŞŤŹ­ŽŻ°ą˛ł´ľśˇ¸šşťź˝žżŔÁÂĂÄĹĆÇČÉĘËĚÍÎĎĐŃŇÓÔŐÖ×ŘŮÚŰÜÝŢßŕáâăäĺćçčéęëěíîďđńňóôőö÷řůúűüýţ˙"}, + {L"Greek (ISO)", "F", 96, L" ‘’£␦␦¦§¨©␦«¬­␦―°±²³΄΅Ά·ΈΉΊ»Ό½ΎΏΐΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡ␦ΣΤΥΦΧΨΩΪΫάέήίΰαβγδεζηθικλμνξοπρςστυφχψωϊϋόύώ␦"}, + {L"Hebrew (ISO)", "H", 96, L" ␦¢£¤¥¦§¨©×«¬­®¯°±²³´µ¶·¸¹÷»¼½¾␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦‗אבגדהוזחטיךכלםמןנסעףפץצקרשת␦␦‎‏␦"}, + {L"Latin-Cyrillic (ISO)", "L", 96, L" ЁЂЃЄЅІЇЈЉЊЋЌ­ЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя№ёђѓєѕіїјљњћќ§ўџ"}, + {L"Latin-5 (ISO)", "M", 96, L" ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏĞÑÒÓÔÕÖ×ØÙÚÛÜİŞßàáâãäåæçèéêëìíîïğñòóôõö÷øùúûüışÿ"}, + {L"Supplemental (DEC)", "%5", 94, L"¡¢£␦¥␦§¤©ª«␦␦␦␦°±²³␦µ¶·␦¹º»¼½␦¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ␦ÑÒÓÔÕÖŒØÙÚÛÜŸ␦ßàáâãäåæçèéêëìíîï␦ñòóôõöœøùúûüÿ␦"}, + {L"Greek (DEC)", "\"?", 94, L"¡¢£␦¥␦§¤©ª«␦␦␦␦°±²³␦µ¶·␦¹º»¼½␦¿ϊΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟ␦ΠΡΣΤΥΦΧΨΩάέήί␦όϋαβγδεζηθικλμνξο␦πρστυφχψωςύώ΄␦"}, + {L"Hebrew (DEC)", "\"4", 94, L"¡¢£␦¥␦§¤©ª«␦␦␦␦°±²³␦µ¶·␦¹º»¼½␦¿␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦אבגדהוזחטיךכלםמןנסעףפץצקרשת␦␦␦␦"}, + {L"Turkish (DEC)", "%0", 94, L"¡¢£␦¥␦§¤©ª«␦␦İ␦°±²³␦µ¶·␦¹º»¼½ı¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏĞÑÒÓÔÕÖŒØÙÚÛÜŸŞßàáâãäåæçèéêëìíîïğñòóôõöœøùúûüÿş"}, + {L"Cyrillic (DEC)", "&4", 94, L"␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦юабцдефгхийклмнопярстужвьызшэщчъЮАБЦДЕФГХИЙКЛМНОПЯРСТУЖВЬЫЗШЭЩЧ"}, + {L"Special Graphics (DEC)", "0", 94, L"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^ ♦▒␉␌␍␊°±␤␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π≠£·"}, + {L"Technical (DEC)", ">", 94, L"⎷┌─⌠⌡│⎡⎣⎤⎦⎛⎝⎞⎠⎨⎬␦␦╲╱␦␦␦␦␦␦␦≤≠≥∫∴∝∞÷Δ∇ΦΓ∼≃Θ×Λ⇔⇒≡ΠΨ␦Σ␦␦√ΩΞΥ⊂⊃∩∪∧∨¬αβχδεφγηιθκλ␦ν∂πψρστ␦ƒωξυζ←↑→↓"}, + {L"U.K. (NRCS)", "A", 94, L"!\"£$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"}, + {L"French (NRCS)", "R", 94, L"!\"£$%&'()*+,-./0123456789:;<=>?àABCDEFGHIJKLMNOPQRSTUVWXYZ°ç§^_`abcdefghijklmnopqrstuvwxyzéùè¨"}, + {L"French Canadian (NRCS)", "9", 94, L"!\"#$%&'()*+,-./0123456789:;<=>?àABCDEFGHIJKLMNOPQRSTUVWXYZâçêî_ôabcdefghijklmnopqrstuvwxyzéùèû"}, + {L"Norwegian/Danish (NRCS)", "`", 94, L"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZÆØÅ^_`abcdefghijklmnopqrstuvwxyzæøå~"}, + {L"Finnish (NRCS)", "5", 94, L"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÅÜ_éabcdefghijklmnopqrstuvwxyzäöåü"}, + {L"German (NRCS)", "K", 94, L"!\"#$%&'()*+,-./0123456789:;<=>?§ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜ^_`abcdefghijklmnopqrstuvwxyzäöüß"}, + {L"Italian (NRCS)", "Y", 94, L"!\"£$%&'()*+,-./0123456789:;<=>?§ABCDEFGHIJKLMNOPQRSTUVWXYZ°çé^_ùabcdefghijklmnopqrstuvwxyzàòèì"}, + {L"Swiss (NRCS)", "=", 94, L"!\"ù$%&'()*+,-./0123456789:;<=>?àABCDEFGHIJKLMNOPQRSTUVWXYZéçêîèôabcdefghijklmnopqrstuvwxyzäöüû"}, + {L"Swedish (NRCS)", "7", 94, L"!\"#$%&'()*+,-./0123456789:;<=>?ÉABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÅÜ_éabcdefghijklmnopqrstuvwxyzäöåü"}, + {L"Spanish (NRCS)", "Z", 94, L"!\"£$%&'()*+,-./0123456789:;<=>?§ABCDEFGHIJKLMNOPQRSTUVWXYZ¡Ñ¿^_`abcdefghijklmnopqrstuvwxyz°ñç~"}, + {L"Portuguese (NRCS)", "%6", 94, L"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZÃÇÕ^_`abcdefghijklmnopqrstuvwxyzãçõ~"}, + {L"Greek (NRCS)", "\">", 94, L"!\"#$%&'()*+,-./0123456789:;<=>?ϊΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟ␦ΠΡΣΤΥΦΧΨΩάέήί␦όϋαβγδεζηθικλμνξο␦πρστυφχψωςύώ΄␦"}, + {L"Hebrew (NRCS)", "%=", 94, L"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_אבגדהוזחטיךכלםמןנסעףפץצקרשת{|}~"}, + {L"Turkish (NRCS)", "%2", 94, L"ı\"#$%ğ'()*+,-./0123456789:;<=>?İABCDEFGHIJKLMNOPQRSTUVWXYZŞÖÇÜ_Ğabcdefghijklmnopqrstuvwxyzşöçü"}, + {L"Russian (NRCS)", "&5", 94, L"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ЮАБЦДЕФГХИЙКЛМНОПЯРСТУЖВЬЫЗШЭЩЧ"}, + +}}; + +charset::charset(const std::wstring_view name, const std::string_view id, const int size, const std::wstring_view glyphs) + : _name{name}, _id{id}, _size{size}, _glyphs{glyphs} +{ +} + +const std::wstring& charset::name() const +{ + return _name; +} + +const std::string& charset::id() const +{ + return _id; +} + +const int charset::size() const +{ + return _size; +} + +const std::wstring& charset::glyphs() const +{ + return _glyphs; +} + +std::vector charset::names() +{ + auto names = std::vector{}; + for (auto& cs : all) + names.emplace_back(cs.name()); + return names; +} + +std::vector charset::names_for_size(const int size) +{ + auto names = std::vector{}; + for (auto& cs : all) + if (cs.size() == size) + names.emplace_back(cs.name()); + return names; +} + +std::optional charset::index_of(const std::string_view id, const int size) +{ + auto index = 0; + for (auto& cs : all) + if (cs.size() == size) { + if (cs.id() == id) return index; + index++; + } + return {}; +} + +const charset* charset::from_index(const int index, const std::optional size) +{ + auto i = 0; + for (auto& cs : all) + if (!size.has_value() || cs.size() == size.value()) { + if (i == index) return &cs; + i++; + } + return {}; +} diff --git a/src/charsets.h b/src/charsets.h new file mode 100644 index 0000000..cee9b0c --- /dev/null +++ b/src/charsets.h @@ -0,0 +1,32 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include +#include +#include +#include +#include + +class charset { +public: + static const std::array all; + + charset(const std::wstring_view name, const std::string_view id, const int size, const std::wstring_view glyphs); + const std::wstring& name() const; + const std::string& id() const; + const int size() const; + const std::wstring& glyphs() const; + static std::vector names(); + static std::vector names_for_size(const int size); + static std::optional index_of(const std::string_view id, const int size); + static const charset* from_index(const int index, const std::optional size = {}); + +private: + std::wstring _name; + std::string _id; + int _size; + std::wstring _glyphs; +}; diff --git a/src/coloring.cpp b/src/coloring.cpp new file mode 100644 index 0000000..e686dee --- /dev/null +++ b/src/coloring.cpp @@ -0,0 +1,48 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "coloring.h" + +#include "capabilities.h" +#include "vt.h" + +coloring::coloring(const capabilities& caps) +{ + // Save the current color table. + _color_table = caps.query_color_table(); + // Set the desired color table entries. + vtout.dcs("2$p" + "0;2;0;0;0/" // Black + "1;2;8;8;8/" // DarkGray + "2;2;19;30;50/" // DarkerBlue + "3;2;23;34;54/" // DarkBlue + "4;2;56;67;87/" // LightBlue + "5;2;58;70;90/" // LighterBlue + "6;2;75;75;75/" // LightGray + "7;2;80;80;80/" // White + "8;2;15;24;40/" // DarkestBlue + "14;2;95;95;95/" // BrightWhite + "15;2;100;100;100/"); // BrightestWhite +} + +coloring::~coloring() +{ + // Restore the original colors, or at least a reasonable palette. + if (!_color_table.empty()) + vtout.dcs("2$p" + _color_table); + else { + vtout.dcs("2$p" + "0;2;0;0;0/" + "1;2;80;14;14/" + "2;2;20;80;20/" + "3;2;80;80;20/" + "4;2;20;20;80/" + "5;2;80;20;80/" + "6;2;20;80;80/" + "7;2;47;47;47/" + "8;2;27;27;27/" + "14;2;0;100;100/" + "15;2;100;100;100"); + } +} diff --git a/src/coloring.h b/src/coloring.h new file mode 100644 index 0000000..4c8c4c1 --- /dev/null +++ b/src/coloring.h @@ -0,0 +1,18 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include + +class capabilities; + +class coloring { +public: + coloring(const capabilities& caps); + ~coloring(); + +private: + std::string _color_table; +}; diff --git a/src/common_dialog.cpp b/src/common_dialog.cpp new file mode 100644 index 0000000..8fcc55e --- /dev/null +++ b/src/common_dialog.cpp @@ -0,0 +1,323 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "common_dialog.h" + +#include "dialog.h" +#include "os.h" + +#include +#include + +using direction = dialog::direction; +using alignment = dialog::alignment; + +namespace { + + class file_entry { + public: + using time_type = std::filesystem::file_time_type; + + file_entry(const std::wstring_view name, const std::filesystem::path& path); + file_entry(const std::filesystem::directory_entry& entry, const bool is_directory); + bool is_file() const; + bool is_directory() const; + const std::wstring& name() const; + void add_to_list(list_control& list) const; + + private: + std::wstring _name; + bool _is_directory = false; + time_type _time = {}; + uintmax_t _size = 0; + }; + + file_entry::file_entry(const std::wstring_view name, const std::filesystem::path& path) + : _name{name}, _is_directory{true} + { + try { + _time = std::filesystem::last_write_time(path); + } catch (...) { + } + } + + file_entry::file_entry(const std::filesystem::directory_entry& entry, const bool is_directory) + : _name{entry.path().filename().wstring()}, _is_directory{is_directory} + { + try { + _time = entry.last_write_time(); + if (!is_directory) _size = entry.file_size(); + } catch (...) { + } + } + + bool file_entry::is_file() const + { + return !_is_directory; + } + + bool file_entry::is_directory() const + { + return _is_directory; + } + + const std::wstring& file_entry::name() const + { + return _name; + } + + void file_entry::add_to_list(list_control& list) const + { + const auto time = std::format(L"{:%Y/%m/%d}", _time); + if (_is_directory) { + list.add({_name, time, L""}); + } else { + static const auto us = std::locale(""); + const auto size = std::format(us, L"{:7L} KB", (_size + 1023) / 1024); + list.add({_name, time, size}); + } + } + + class file_dialog { + public: + enum class type { + save, + open + }; + + file_dialog(const type type); + std::filesystem::path show(const std::filesystem::path& folder, const std::wstring_view name); + + private: + bool _load_entries(const std::filesystem::path& folder); + bool _handle_key(const key key_press); + void _handle_list_change(); + bool _validate_selection() const; + std::filesystem::path _selected_path() const; + static std::wstring _path_string(const std::filesystem::path& path, const size_t max_length); + + type _type; + dialog _dlg; + text_control& _folder_field; + list_control& _list_field; + input_control& _name_field; + std::vector _entries; + std::filesystem::path _selected_folder; + }; + + file_dialog::file_dialog(const type type) + : _type{type}, + _dlg{type == type::open ? L"Open" : L"Save As"}, + _folder_field{_dlg.add_text(L"")}, + _list_field{_dlg.add_list({L"Name", L"Date", L"Size"}, {24, 10, 10}, 9)}, + _name_field{_dlg.add_input(L"Filename", 40)} + { + auto& buttons = _dlg.add_group(alignment::right); + buttons.add_button(type == type::open ? L"Open" : L"Save", 1, true); + buttons.add_button(L"Cancel", 2); + + _list_field.on_key_press([&](const auto key_press) { + return _handle_key(key_press); + }); + _list_field.on_change([&] { + return _handle_list_change(); + }); + _dlg.on_validate([&](const int return_code) { + return return_code == 2 || _validate_selection(); + }); + _dlg.focus(_name_field); + } + + std::filesystem::path file_dialog::show(const std::filesystem::path& folder, const std::wstring_view name) + { + _name_field.value(name); + if (_load_entries(folder)) { + if (_dlg.show() == 1) { + const auto selected_path = _selected_path(); + if (selected_path.empty()) return {}; + std::filesystem::current_path(_selected_folder); + return selected_path; + } + } + return {}; + } + + bool file_dialog::_load_entries(const std::filesystem::path& folder) + try { + auto folders = std::vector{}; + if (folder.has_parent_path() && folder.has_relative_path()) + folders.emplace_back(L"..", folder.parent_path()); + + auto files = std::vector{}; + for (const auto& entry : std::filesystem::directory_iterator(folder)) { + if (!os::is_file_hidden(entry.path())) { + if (entry.is_directory()) + folders.emplace_back(entry, true); + else + files.emplace_back(entry, false); + } + } + + const auto selected_folder = std::find_if(folders.begin(), folders.end(), [&](const auto& folder) { + return folder.name() == _selected_folder.filename(); + }); + + _entries = folders; + _entries.insert(_entries.end(), files.begin(), files.end()); + _selected_folder = folder; + _folder_field.value(_path_string(folder, 50)); + _list_field.clear(); + for (const auto& entry : _entries) + entry.add_to_list(_list_field); + + if (selected_folder != folders.end()) { + const auto selected_offset = std::distance(folders.begin(), selected_folder); + _list_field.selection(selected_offset); + } + + return true; + } catch (...) { + using id = common_dialog::id; + const auto title = folder.filename().wstring(); + const auto message = L"You don't currently have permission to\naccess this folder."; + common_dialog::message_box(title, message, id::cancel); + return false; + } + + bool file_dialog::_handle_key(const key key_press) + { + if (key_press == key::enter) { + const auto selection = _list_field.selection(); + const auto& selected_entry = _entries[selection]; + if (selected_entry.is_directory()) { + auto selected_path = _selected_folder; + if (selected_entry.name() == L"..") + selected_path = selected_path.parent_path(); + else + selected_path.append(selected_entry.name()); + _load_entries(selected_path); + return true; + } + } else if (key_press == key::bksp) { + if (_selected_folder.has_parent_path() && _selected_folder.has_relative_path()) { + _load_entries(_selected_folder.parent_path()); + return true; + } + } + return false; + } + + void file_dialog::_handle_list_change() + { + const auto selection = _list_field.selection(); + const auto& selected_entry = _entries[selection]; + if (selected_entry.is_file()) + _name_field.value(selected_entry.name()); + } + + bool file_dialog::_validate_selection() const + { + using id = common_dialog::id; + const auto selected_path = _selected_path(); + if (selected_path.empty()) return false; + const auto name = selected_path.filename().wstring(); + const auto exists = std::filesystem::exists(selected_path); + if (_type == type::open && !exists) { + const auto title = L"Open"; + const auto message = name + L"\nFile not found.\nCheck the filename and try again."; + common_dialog::message_box(title, message, id::ok); + return false; + } + if (_type == type::save && exists) { + const auto title = L"Confirm Save As"; + const auto message = name + L" already exists.\nDo you want to replace it?"; + const auto choice = common_dialog::message_box(title, message, id::yes | id::no); + return choice == id::yes; + } + return true; + } + + std::filesystem::path file_dialog::_selected_path() const + { + const auto selected_name = _name_field.value(); + if (selected_name.empty()) return {}; + auto selected_path = _selected_folder; + return selected_path.append(selected_name); + } + + std::wstring file_dialog::_path_string(const std::filesystem::path& path, const size_t max_length) + { + constexpr auto prefix = L"> "; + auto full_path = prefix + path.wstring(); + if (full_path.length() <= max_length) + return full_path; + else { + auto segments = std::vector(); + for (auto segment : path.relative_path()) + segments.emplace_back(segment.wstring()); + + const auto separator = static_cast(path.preferred_separator); + auto root = prefix + path.root_name().wstring() + separator + L"..."; + auto total_length = root.length(); + auto keep = segments.size(); + while (keep > 0 && total_length + segments[keep - 1].length() < max_length) + total_length += segments[--keep].length() + 1; + + for (auto i = keep; i < segments.size(); i++) + root += separator + segments[i]; + return root.substr(0, max_length); + } + } + +} // namespace + +namespace common_dialog { + + std::filesystem::path open() + { + const auto folder = std::filesystem::current_path(); + auto dlg = file_dialog{file_dialog::type::open}; + return dlg.show(folder, L""); + } + + std::filesystem::path save(const std::filesystem::path& filepath) + { + const auto folder = filepath.parent_path(); + const auto name = filepath.filename().wstring(); + auto dlg = file_dialog{file_dialog::type::save}; + return dlg.show(folder, name); + } + + int message_box(const std::wstring_view title, const std::wstring_view message, const int buttons) + { + auto dlg = dialog{title}; + const auto max_width = dlg.max_width() - 2; + const auto wrap_text = [&dlg, max_width](const auto text) { + auto off = 0; + while (text.length() > off + max_width) { + dlg.add_text(text.substr(off, max_width)); + off += max_width; + } + dlg.add_text(text.substr(off)); + }; + for (auto start = 0, end = 0; end = message.find('\n', start); start = end + 1) { + if (end == std::wstring::npos) { + wrap_text(message.substr(start)); + break; + } + wrap_text(message.substr(start, end - start)); + } + auto& group = dlg.add_group(alignment::center); + if (buttons & id::yes) + group.add_button(L"Yes", id::yes, true); + if (buttons & id::no) + group.add_button(L"No", id::no); + if (buttons & id::ok) + group.add_button(L"OK", id::ok, true); + if (buttons & id::cancel) + group.add_button(L"Cancel", id::cancel); + return dlg.show(); + } + +} // namespace common_dialog diff --git a/src/common_dialog.h b/src/common_dialog.h new file mode 100644 index 0000000..a3ec9ea --- /dev/null +++ b/src/common_dialog.h @@ -0,0 +1,24 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include +#include + +namespace common_dialog { + + std::filesystem::path open(); + std::filesystem::path save(const std::filesystem::path& filepath); + + enum id { + yes = 1, + no = 2, + ok = 4, + cancel = 8 + }; + + int message_box(const std::wstring_view title, const std::wstring_view message, const int buttons); + +} // namespace common_dialog diff --git a/src/dialog.cpp b/src/dialog.cpp new file mode 100644 index 0000000..8e8e4f3 --- /dev/null +++ b/src/dialog.cpp @@ -0,0 +1,1051 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "dialog.h" + +#include "capabilities.h" +#include "macros.h" +#include "vt.h" + +#include +#include + +namespace color { + + constexpr auto title = {0, 1, 7, 47, 30}; // White on DarkestBlue + constexpr auto basic = {0, 1, 7, 40, 36}; // Black on BrightWhite + constexpr auto borders = {1, 46, 36}; // LightGray on BrightWhite + constexpr auto input = {1, 40, 37}; // Black on BrightestWhite (assumes reverse) + constexpr auto input_label = {36}; // BrightWhite background (assumes bright reverse) + constexpr auto list_header = {22, 36}; // LightGray background (assumes reverse) + constexpr auto selected = {22, 35}; // LighterBlue background (assumes reverse) + constexpr auto unselected = {1, 37}; // BrightestWhite background (assumes reverse) + constexpr auto button = {22, 7, 40, 36}; // Black on LightGray + constexpr auto button_focus = {1, 27, 36, 42}; // BrightWhite on DarkerBlue + +} // namespace color + +namespace { + + namespace macros { + + int draw_frame() + { + static int frame_macro = macro_manager::create([](auto& macro) { + macro.sgr(color::basic); + macro.decfra(' ', {}, {}, {}, {}); + + macro.ls1(); + macro.decfra('[', {}, {}, {}, 1); + macro.decfra(']', {}, 99, {}, 99); + macro.decfra('-', 99, 1, 99, 99); + macro.cup(99, {}); + macro.write('`'); + macro.cuf(99); + macro.write('\''); + macro.ls0(); + + macro.sgr(color::title); + macro.decfra(' ', {}, {}, 1, {}); + }); + return frame_macro; + } + + } // namespace macros + + std::wstring pad(const std::wstring& s, const int width) + { + auto padded = s.substr(0, width); + padded.insert(padded.length(), width - padded.length(), ' '); + return padded; + } + + static auto search_string = std::wstring{}; + static auto last_search_time = std::chrono::system_clock::time_point{}; + + std::optional search(const wchar_t ch, const int current, int size, std::function supplier) + { + using namespace std::chrono_literals; + const auto now = std::chrono::system_clock::now(); + auto base_index = current; + if (now > last_search_time + 500ms) { + search_string.clear(); + base_index++; + } + last_search_time = now; + search_string += std::tolower(ch); + for (auto i = 0; i < size; i++) { + const auto index = (base_index + i) % size; + const auto item = supplier(index); + auto match = 0; + while (match < item.length() && match < search_string.length()) { + if (std::tolower(item[match]) != search_string[match]) break; + match++; + } + if (match == search_string.length()) + return index; + } + return {}; + } + + static int screen_height = 24; + static int screen_width = 80; + +} // namespace + +borders::borders(const int top, const int left, const int bottom, const int right) + : _top{top}, _left{left}, _height{bottom - top + 1}, _width{right - left + 1}, + _horizontal(_height * _width) +{ +} + +borders::~borders() +{ + if (!_vertical.empty()) { + vtout.decsc(); + vtout.sgr(color::borders); + vtout.ls1(); + for (auto y = 0, off = 0; y < _height; y++) { + for (auto x = 0; x < _width;) { + const auto type = _horizontal[x + off]; + auto x2 = x + 1; + while (x2 < _width && _horizontal[x2 + off] == type) + x2++; + if (type != 0) { + const auto top = _top + y; + const auto left = _left + x; + const auto right = left + (x2 - x) - 1; + const auto ch = " -~="[type]; + vtout.decfra(ch, top, left, top, right); + } + x = x2; + } + off += _width; + } + vtout.sgr({37}); + for (auto [column, top, bottom, ch] : _vertical) { + if (bottom > top) + vtout.decfra(ch, top, column, bottom, column); + else { + vtout.cup(top, column); + vtout.write(ch); + } + } + vtout.decrc(); + } +} + +void borders::horizontal(const int row, const int left, const int right, const int type) +{ + auto off = (row - _top) * _width + (left - _left); + for (auto x = left; x <= right; x++) + _horizontal[off++] |= type; +} + +void borders::vertical(const int column, const int top, const int bottom, const char ch) +{ + _vertical.emplace_back(column, top, bottom, ch); +} + +void borders::all(const int top, const int left, const int bottom, const int right, const char lch, const char rch, const bool include_top) +{ + if (include_top) + horizontal(top - 1, left, right, 1); + horizontal(bottom + 1, left, right, 2); + vertical(left, top, bottom, lch); + vertical(right, top, bottom, rch); +} + +control::control(layout& parent, const bool can_focus) + : _parent{parent}, _can_focus{can_focus} +{ +} + +void control::on_key_press(const std::function handler) +{ + _key_handler = handler; +} + +void control::on_change(const std::function handler) +{ + _change_handler = handler; +} + +int control::_min_height() const +{ + return 1; +} + +int control::_min_width() const +{ + return 1; +} + +void control::_reposition(const int row, const int col, const int height, const int width) +{ + _height = height; + _width = width; + _top = row; + _left = col; + _bottom = _top + _height - 1; + _right = _left + _width - 1; +} + +void control::_instantiate(borders& borders) +{ +} + +void control::_redraw(const bool focused) +{ +} + +void control::_focus(const bool focused) +{ +} + +bool control::_handle_key(const key key_press) +{ + if (&_parent == this) + return false; + else if (_key_handler && _key_handler(key_press)) + return true; + else + return _parent._handle_key(key_press); +} + +void control::_notify_change() +{ + if (_change_handler) _change_handler(); +} + +void control::_dirty() +{ + _parent.dlg()._dirty_control(this); +} + +text_control::text_control(const std::wstring_view value, layout& parent) + : control{parent, false}, _value{value} +{ +} + +void text_control::value(const std::wstring_view value) +{ + _value = value; + _dirty(); +} + +int text_control::_min_width() const +{ + return _value.length(); +} + +void text_control::_instantiate(borders& borders) +{ + vtout.cup(_top, _left); + vtout.sgr(color::basic); + vtout.write(_value.substr(0, _width)); +} + +void text_control::_redraw(const bool focused) +{ + vtout.cup(_top, _left); + vtout.sgr(color::basic); + vtout.write(_value.substr(0, _width)); + vtout.write_spaces(_width - _value.length()); +} + +input_control::input_control(const std::wstring_view label, const int width, const int& label_space, layout& parent) + : control{parent, true}, _label{label}, _input_width{width}, _label_space{label_space} +{ +} + +void input_control::value(const std::wstring_view value) +{ + _value = value; + _cursor = 0; + _scroll = 0; + _dirty(); +} + +const std::wstring& input_control::value() const +{ + return _value; +} + +int input_control::_min_width() const +{ + return _label.length() + 1 + _input_width; +} + +void input_control::_reposition(const int row, const int col, const int height, const int width) +{ + _label_width = std::max(_label_space, _label.length()); + control::_reposition(row, col + _label_width + 2, height, width - _label_width - 3); +} + +void input_control::_instantiate(borders& borders) +{ + vtout.cup(_top, _left - _label_width - 2); + vtout.sgr(color::input_label); + vtout.write(_label); + vtout.deccara(_top, _left - 1, _bottom, _right + 1, color::input); + vtout.cup(_top, _left); + vtout.sgr(color::input); + vtout.write(_value.substr(0, _width)); + borders.all(_top, _left - 1, _bottom, _right + 1, '[', ']'); +} + +void input_control::_redraw(const bool focused) +{ + vtout.cup(_top, _left); + vtout.sgr(color::input); + vtout.write(_value.substr(0, _width)); + vtout.write_spaces(_width - _value.length()); +} + +void input_control::_focus(const bool focused) +{ + if (focused) { + const auto off = _cursor - _scroll; + vtout.decslrm({}, _right); + vtout.cup(_bottom, _left + off); + vtout.sm('?', 25); + vtout.sm(4); + vtout.sgr(color::input); + } else { + vtout.rm('?', 25); + vtout.rm(4); + vtout.decslrm(); + } +} + +bool input_control::_handle_key(const key key_press) +{ + if (const auto ch = keyboard::printable(key_press)) { + vtout.write(ch.value()); + _value.insert(_cursor, 1, ch.value()); + _pan_right(true); + return true; + } else { + switch (key_press) { + case key::bksp: + if (_cursor > 0) { + if (_scroll > 0) + _erase_back(--_cursor); + else { + vtout.write('\b'); + _erase(--_cursor); + } + } + return true; + case key::del: + if (_cursor < _value.length()) + _erase(_cursor); + return true; + case key::left: + if (_cursor > 0) + _pan_left(); + return true; + case key::right: + if (_cursor < _value.length()) + _pan_right(); + return true; + default: + return control::_handle_key(key_press); + } + } +} + +void input_control::_pan_left() +{ + _cursor--; + if (_cursor < _scroll) { + _scroll--; + vtout.rm('?', 25); + vtout.write(_value[_cursor]); + vtout.write('\b'); + vtout.sm('?', 25); + } else { + vtout.write('\b'); + } +} + +void input_control::_pan_right(const bool already_moved) +{ + _cursor++; + if (_cursor >= _width + _scroll) { + _scroll++; + vtout.deccra(_bottom, _left + 1, _bottom, _right, _bottom, _left); + vtout.write(_char_at(_cursor)); + } else if (!already_moved) { + vtout.decfi(); + } +} + +void input_control::_erase(const int index) +{ + _value.erase(index, 1); + const auto rx1 = _left + index - _scroll; + const auto rx2 = _left + _width - 1; + if (rx1 < rx2) + vtout.deccra(_bottom, rx1 + 1, _bottom, rx2, _bottom, rx1); + vtout.decfra(_char_at(_scroll + _width - 1), _bottom, rx2, _bottom, rx2); +} + +void input_control::_erase_back(const int index) +{ + _value.erase(index, 1); + const auto rx2 = _left + index - _scroll; + _scroll--; + if (rx2 >= _left) { + if (rx2 > _left) + vtout.deccra(_bottom, _left, _bottom, rx2 - 1, _bottom, _left + 1); + vtout.decfra(_char_at(_scroll), _bottom, _left, _bottom, _left); + } +} + +wchar_t input_control::_char_at(const int index) const +{ + if (index >= 0 && index < _value.length()) + return _value[index]; + else + return L' '; +} + +list_control::list_control(const std::initializer_list headers, const std::initializer_list widths, const int max_rows, layout& parent) + : control{parent, true}, _widths{widths}, _max_rows{max_rows} +{ + for (auto header : headers) + _headers.emplace_back(header); +} + +void list_control::clear() +{ + _render_selection(false); + _selection = 0; + _scroll = 0; + _rows.clear(); + _dirty(); + last_search_time = {}; +} + +void list_control::add(const std::initializer_list values) +{ + auto& row = _rows.emplace_back(); + for (auto value : values) + row.emplace_back(value); + _dirty(); +} + +int list_control::selection() const +{ + return _selection; +} + +void list_control::selection(const int index) +{ + _selection = std::max(std::min(index, _rows.size() - 1), 0); + _scroll = std::max(_selection - (_max_rows - 1), 0); + _dirty(); + _notify_change(); +} + +int list_control::list_control::_min_height() const +{ + return _max_rows + 1; +} + +int list_control::list_control::_min_width() const +{ + auto total = 0; + for (auto width : _widths) + total += (width + 2); + return total; +} + +void list_control::list_control::_instantiate(borders& borders) +{ + _render_row(0, _headers, false); + vtout.deccara(_top, _left, _top, _right, color::list_header); + vtout.sgr({22, 36, 47}); + vtout.ls1(); + for (auto i = 0, column = _left; i < _headers.size(); i++) { + if (i > 0) { + vtout.cup(_top, column); + vtout.write('['); + borders.vertical(column, _top + 1, _bottom, '['); + } + column += _widths[i] + 2; + } + vtout.ls0(); + vtout.deccara(_top + 1, _left, _bottom, _right, color::input); + vtout.sgr(color::input); + for (auto i = 0; i < _rows.size() && i < _max_rows; i++) + _render_row(i + 1, _rows[i], false); + borders.all(_top + 1, _left, _bottom, _right, '[', ']', false); +} + +void list_control::_redraw(const bool focused) +{ + using namespace std::string_literals; + const auto columns = _headers.size(); + const auto empty_row = std::vector(columns, L""s); + vtout.sgr(color::input); + for (auto i = 0; i < _max_rows; i++) { + const auto index = _scroll + i; + if (index < _rows.size()) + _render_row(i + 1, _rows[index], true); + else + _render_row(i + 1, empty_row, true); + } + if (focused) _render_selection(true); +} + +void list_control::_focus(const bool focused) +{ + _render_selection(focused); + if (focused) _notify_change(); +} + +bool list_control::_handle_key(const key key_press) +{ + switch (key_press) { + case key::up: + _move_to(_selection - 1); + return true; + case key::down: + _move_to(_selection + 1); + return true; + case key::home: + _move_to(0); + return true; + case key::end: + _move_to(_rows.size() - 1); + return true; + case key::pgup: + if (_selection > _scroll) + _move_to(_scroll); + else + _move_to(_selection - _max_rows + 1); + return true; + case key::pgdn: + if (_selection + 1 < _scroll + _max_rows) + _move_to(_scroll + _max_rows - 1); + else + _move_to(_selection + _max_rows - 1); + return true; + default: + if (const auto ch = keyboard::printable(key_press)) { + _search(ch.value()); + return true; + } + return control::_handle_key(key_press); + } +} + +void list_control::_search(const wchar_t ch) +{ + const auto match = search(ch, _selection, _rows.size(), [&](const auto index) { + return _rows[index][0]; + }); + if (match) _move_to(match.value()); +} + +void list_control::_move_to(const int index) +{ + const auto new_selection = std::max(std::min(index, _rows.size() - 1), 0); + if (_selection != new_selection) { + _render_selection(false); + _selection = new_selection; + if (_selection < _scroll) { + const auto diff = _scroll - _selection; + _scroll -= diff; + if (diff < _max_rows) + vtout.deccra(_top + 1, _left, _bottom - diff, _right, _top + 1 + diff, _left); + for (auto i = 0; i < diff && i < _max_rows; i++) + _render_row(1 + i, _rows[_scroll + i], true); + } else if (_selection > _scroll + _max_rows - 1) { + const auto diff = _selection - (_scroll + _max_rows - 1); + _scroll += diff; + if (diff < _max_rows) + vtout.deccra(_top + 1 + diff, _left, _bottom, _right, _top + 1, _left); + for (auto i = std::max(_max_rows - diff, 0); i < _max_rows; i++) + _render_row(1 + i, _rows[_scroll + i], true); + } + _render_selection(true); + _notify_change(); + } +} + +void list_control::list_control::_render_row(const int y, const std::vector& values, const bool fill) +{ + vtout.cup(_top + y, _left + 1); + for (auto i = 0; i < values.size(); i++) { + const auto width = _widths[i]; + const auto value = fill ? pad(values[i], width) : values[i].substr(0, width); + vtout.write(value); + if (i + 1 < values.size()) + vtout.cuf(int(width - value.length() + 2)); + } +} + +void list_control::_render_selection(const bool selected) +{ + const auto y = _top + 1 + _selection - _scroll; + const auto attrs = selected ? color::selected : color::unselected; + vtout.deccara(y, _left, y, _right, attrs); +} + +dropdown_control::dropdown_control(const std::wstring_view label, const std::vector& options, const int& label_space, layout& parent) + : control{parent, true}, _label{label}, _label_space{label_space} +{ + for (auto option : options) + _options.emplace_back(option); +} + +void dropdown_control::options(const std::vector& options) +{ + _options.clear(); + for (auto option : options) + _options.emplace_back(option); + _selection = 0; + _dirty(); + _notify_change(); + last_search_time = {}; +} + +int dropdown_control::selection() const +{ + return _selection; +} + +void dropdown_control::selection(const int index) +{ + _selection = std::max(std::min(index, _options.size() - 1), 0); + _dirty(); + _notify_change(); +} + +int dropdown_control::_min_width() const +{ + const auto max = [](auto acc, auto& option) { return std::max(acc, option.length()); }; + const auto input_width = std::accumulate(_options.begin(), _options.end(), 0, max) + 3; + return _label.length() + 1 + input_width; +} + +void dropdown_control::_reposition(const int row, const int col, const int height, const int width) +{ + _label_width = std::max(_label_space, _label.length()); + control::_reposition(row, col + _label_width + 1, height, width - _label_width - 1); +} + +void dropdown_control::_instantiate(borders& borders) +{ + vtout.cup(_top, _left - _label_width - 1); + vtout.sgr(color::input_label); + vtout.write(_label); + vtout.sgr(color::input); + _redraw(false); + borders.all(_top, _left, _bottom, _right, '[', 'v'); +} + +void dropdown_control::_redraw(const bool focused) +{ + const auto option = pad(_options[_selection], _width - 2); + _focus(focused); + vtout.sgr(focused ? color::selected : color::unselected); + vtout.cup(_bottom, _left + 1); + vtout.write(option); +} + +void dropdown_control::_focus(const bool focused) +{ + const auto attrs = focused ? color::selected : color::unselected; + vtout.deccara(_top, _left, _top, _right - 1, attrs); +} + +bool dropdown_control::_handle_key(const key key_press) +{ + switch (key_press) { + case key::up: + _move_to(_selection - 1); + return true; + case key::down: + _move_to(_selection + 1); + return true; + case key::home: + _move_to(0); + return true; + case key::end: + _move_to(_options.size() - 1); + return true; + default: + if (const auto ch = keyboard::printable(key_press)) { + _search(ch.value()); + return true; + } + return control::_handle_key(key_press); + } +} + +void dropdown_control::_search(const wchar_t ch) +{ + const auto match = search(ch, _selection, _options.size(), [&](const auto index) { + return _options[index]; + }); + if (match) _move_to(match.value()); +} + +void dropdown_control::_move_to(const int index) +{ + const auto new_selection = std::max(std::min(index, _options.size() - 1), 0); + if (_selection != new_selection) { + _selection = new_selection; + _redraw(true); + _notify_change(); + } +} + +button_control::button_control(const std::wstring_view label, const int id, layout& parent) + : control{parent, true}, _label{label}, _id{id} +{ +} + +int button_control::_min_width() const +{ + const auto pad = std::max(10 - _label.length(), 2) & ~1; + return _label.length() + pad; +} + +void button_control::_instantiate(borders& borders) +{ + _focus(false); + const int indent = (_width - _label.length()) / 2; + vtout.cup(_top, _left + indent); + vtout.sgr(color::button); + vtout.write(_label); +} + +void button_control::_focus(const bool focused) +{ + const auto attrs = focused ? color::button_focus : color::button; + vtout.deccara(_top, _left, _top, _right, attrs); +} + +bool button_control::_handle_key(const key key_press) +{ + switch (key_press) { + case key::enter: + case key::space: + _parent.dlg().close(_id); + return true; + default: + return control::_handle_key(key_press); + } +} + +layout::layout(dialog& dlg, layout& parent) + : control{parent, false}, _dlg{dlg} +{ +} + +dialog& layout::dlg() +{ + return _dlg; +} + +text_control& layout::add_text(const std::wstring_view value) +{ + return _add_control(value, *this); +} + +input_control& layout::add_input(const std::wstring_view label, const int width) +{ + if (_direction == direction::top_to_bottom && _controls.size()) add_gap(); + auto& control = _add_control(label, width, _label_width, *this); + _track_label_width(label, control); + return control; +} + +list_control& layout::add_list(const std::initializer_list headers, const std::initializer_list widths, const int height) +{ + return _add_control(headers, widths, height, *this); +} + +dropdown_control& layout::add_dropdown(const std::wstring_view label, const std::vector& options) +{ + if (_direction == direction::top_to_bottom && _controls.size()) add_gap(); + auto& control = _add_control(label, options, _label_width, *this); + _track_label_width(label, control); + return control; +} + +button_control& layout::add_button(const std::wstring_view label, const int id, const bool is_default) +{ + auto& button = _add_control(label, id, *this); + if (is_default) _dlg._default_return_code = id; + return button; +} + +layout& layout::add_group(const alignment halign) +{ + auto& control_ref = _add_control(_dlg, *this); + control_ref._direction = direction(1 - int(_direction)); + control_ref._valign = alignment::top; + control_ref._halign = halign; + if (_controls.size() > 1 && _direction == direction::top_to_bottom) + control_ref._margin_top = 1; + return control_ref; +} + +void layout::add_gap() +{ + _add_control(*this, false); +} + +int layout::_min_height() const +{ + if (_direction == direction::top_to_bottom) { + const auto sum = [](auto acc, auto& control) { return acc + control->_min_height(); }; + return _margin_top + std::accumulate(_controls.begin(), _controls.end(), 0, sum) + _margin_bottom; + } else { + const auto max = [](auto acc, auto& control) { return std::max(acc, control->_min_height()); }; + return _margin_top + std::accumulate(_controls.begin(), _controls.end(), 0, max) + _margin_bottom; + } +} + +int layout::_min_width() const +{ + const auto init = _label_width + _input_width; + if (_direction == direction::top_to_bottom) { + const auto max = [](auto acc, auto& control) { return std::max(acc, control->_min_width()); }; + return _margin_left + std::accumulate(_controls.begin(), _controls.end(), init, max) + _margin_right; + } else { + const auto sum = [](auto acc, auto& control) { return acc + control->_min_width() + 1; }; + return _margin_left + std::accumulate(_controls.begin(), _controls.end(), init, sum) - 1 + _margin_right; + } +} + +void layout::_reposition(const int row, const int col, const int height, const int width) +{ + const auto used_height = _valign == alignment::fill ? height : this->_min_height(); + const auto used_width = _halign == alignment::fill ? width : this->_min_width(); + const auto top = row + _offset(height, used_height, _valign); + const auto left = col + _offset(width, used_width, _halign); + control::_reposition(top, left, used_height, used_width); + + auto r = _top + _margin_top; + auto c = _left + _margin_left; + const auto inner_width = _width - _margin_left - _margin_right; + const auto inner_height = _height - _margin_top - _margin_bottom; + for (auto& control : _controls) { + if (_direction == direction::top_to_bottom) { + control->_reposition(r, c, control->_min_height(), inner_width); + r += control->_min_height(); + } else { + control->_reposition(r, c, inner_height, control->_min_width()); + c += control->_min_width() + 1; + } + } +} + +void layout::_instantiate(borders& borders) +{ + for (auto& control : _controls) { + if (control->_can_focus) { + _arrow_order.push_back(control.get()); + _dlg._tab_order.push_back(control.get()); + } + control->_instantiate(borders); + } +} + +bool layout::_handle_key(const key key_press) +{ + const auto forward = _direction == direction::left_to_right ? key::right : key::down; + if (key_press == forward) { + auto index = _arrow_index() + 1; + if (index < _arrow_order.size()) + _dlg._focus_control(_arrow_order[index]); + return true; + } + const auto back = _direction == direction::left_to_right ? key::left : key::up; + if (key_press == back) { + auto index = _arrow_index() - 1; + if (index >= 0) + _dlg._focus_control(_arrow_order[index]); + return true; + } + return control::_handle_key(key_press); +} + +template +T& layout::_add_control(Types&&... args) +{ + auto control_ptr = std::make_unique(std::forward(args)...); + auto& control_ref = *control_ptr; + _controls.push_back(std::move(control_ptr)); + return control_ref; +} + +void layout::_track_label_width(const std::wstring_view label, const control& control) +{ + if (_direction == direction::top_to_bottom) { + _label_width = std::max(_label_width, label.length()); + _input_width = std::max(_input_width, control._min_width() - label.length()); + } +} + +int layout::_offset(const int available, const int used, const alignment align) +{ + switch (align) { + case alignment::left: + return 0; + case alignment::center: + return (available - used) / 2; + case alignment::right: + return available - used; + default: + return 0; + } +} + +int layout::_arrow_index() const +{ + const auto position = std::find(_arrow_order.begin(), _arrow_order.end(), &_dlg.focus()); + return std::distance(_arrow_order.begin(), position); +} + +void dialog::initialize(const capabilities& caps) +{ + screen_height = caps.height; + screen_width = caps.width; + + // Force macro instantiation. + macros::draw_frame(); +} + +dialog::dialog(const std::wstring_view title) + : layout{*this, *this}, _title{title} +{ + _margin_top = 2; + _margin_bottom = 1; + _margin_left = 2; + _margin_right = 2; +} + +int dialog::show() +{ + _reposition(1, 1, screen_height, screen_width); + + static int page = 1; + page++; + + vtout.decstbm(_top, _bottom); + vtout.decslrm(_left, _right); + vtout.deccra({}, {}, {}, {}, 1, {}, {}, page); + vtout.decinvm(macros::draw_frame()); + + vtout.cup({}, int(screen_width - _title.length()) / 2 + 2 - _left); + vtout.write(_title); + vtout.sgr(color::basic); + + vtout.decslrm(); + vtout.decstbm(); + + { + auto b = borders{_top, _left, _bottom, _right}; + _instantiate(b); + } + + _focus_control(_initial_focused_control ? _initial_focused_control : _tab_order[0]); + _return_code = {}; + while (!_return_code) { + _dirty_controls.clear(); + _focused_control->_handle_key(keyboard::read()); + for (auto control : _dirty_controls) + control->_redraw(_focused_control == control); + } + _focus_control(nullptr); + + vtout.deccra(_top, _left, _bottom, _right, page, _top, _left, 1); + page--; + + return _return_code.value(); +} + +void dialog::close(const int id) +{ + _return_code = id; + if (_validate_handler) { + if (_focused_control) _focused_control->_focus(false); + if (!_validate_handler(id)) { + _return_code = {}; + if (_focused_control) _focused_control->_focus(true); + } + } +} + +void dialog::focus(control& control) +{ + _initial_focused_control = &control; +} + +const control& dialog::focus() const +{ + return *_focused_control; +} + +int dialog::max_width() const +{ + return std::min(screen_width * 80 / 100, 50); +} + +void dialog::on_validate(const std::function handler) +{ + _validate_handler = handler; +} + +bool dialog::_handle_key(const key key_press) +{ + switch (key_press) { + case key::tab: { + const auto index = _tab_index() + 1; + _focus_control(_tab_order[index % _tab_order.size()]); + return true; + } + case key::shift + key::tab: { + const auto index = _tab_index() + _tab_order.size() - 1; + _focus_control(_tab_order[index % _tab_order.size()]); + return true; + } + case key::enter: + if (_default_return_code) + close(_default_return_code.value()); + return true; + default: + return layout::_handle_key(key_press); + } +} + +int dialog::_tab_index() const +{ + const auto position = std::find(_tab_order.begin(), _tab_order.end(), _focused_control); + return std::distance(_tab_order.begin(), position); +} + +void dialog::_focus_control(control* control) +{ + if (_focused_control != control) { + if (_focused_control) _focused_control->_focus(false); + _focused_control = control; + if (_focused_control) _focused_control->_focus(true); + } +} + +void dialog::_dirty_control(control* control) +{ + if (std::find(_dirty_controls.begin(), _dirty_controls.end(), control) == _dirty_controls.end()) + _dirty_controls.push_back(control); +} diff --git a/src/dialog.h b/src/dialog.h new file mode 100644 index 0000000..1317070 --- /dev/null +++ b/src/dialog.h @@ -0,0 +1,270 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include "keyboard.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +class capabilities; +class layout; +class dialog; + +class borders { +public: + borders(const int top, const int left, const int bottom, const int right); + ~borders(); + void horizontal(const int row, const int left, const int right, const int type); + void vertical(const int column, const int top, const int bottom, const char ch); + void all(const int top, const int left, const int bottom, const int right, const char lch, const char rch, const bool include_top = true); + +private: + int _top; + int _left; + int _height; + int _width; + std::vector _horizontal; + std::vector> _vertical; +}; + +class control { +public: + control(layout& parent, const bool can_focus); + virtual ~control() = default; + void on_key_press(const std::function handler); + void on_change(const std::function handler); + +protected: + friend layout; + friend dialog; + + virtual int _min_height() const; + virtual int _min_width() const; + virtual void _reposition(const int row, const int col, const int height, const int width); + virtual void _instantiate(borders& borders); + virtual void _redraw(const bool focused); + virtual void _focus(const bool focused); + virtual bool _handle_key(const key key_press); + + void _notify_change(); + void _dirty(); + + layout& _parent; + bool _can_focus = false; + int _top = 0; + int _left = 0; + int _bottom = 0; + int _right = 0; + int _height = 0; + int _width = 0; + std::function _key_handler; + std::function _change_handler; +}; + +class text_control : public control { +public: + text_control(const std::wstring_view value, layout& parent); + void value(const std::wstring_view value); + +private: + virtual int _min_width() const; + virtual void _instantiate(borders& borders); + virtual void _redraw(const bool focused); + + std::wstring _value; +}; + +class input_control : public control { +public: + input_control(const std::wstring_view label, const int width, const int& label_space, layout& parent); + void value(const std::wstring_view value); + const std::wstring& value() const; + +private: + virtual int _min_width() const; + virtual void _reposition(const int row, const int col, const int height, const int width); + virtual void _instantiate(borders& borders); + virtual void _redraw(const bool focused); + virtual void _focus(const bool focused); + virtual bool _handle_key(const key key_press); + + void _pan_left(); + void _pan_right(const bool already_moved = false); + void _erase(const int index); + void _erase_back(const int index); + wchar_t _char_at(const int index) const; + + const int& _label_space; + std::wstring _label; + std::wstring _value; + int _label_width = 0; + int _input_width = 0; + int _cursor = 0; + int _scroll = 0; +}; + +class list_control : public control { +public: + list_control(const std::initializer_list headers, const std::initializer_list widths, const int max_rows, layout& parent); + void clear(); + void add(const std::initializer_list values); + int selection() const; + void selection(const int index); + +private: + virtual int _min_height() const; + virtual int _min_width() const; + virtual void _instantiate(borders& borders); + virtual void _redraw(const bool focused); + virtual void _focus(const bool focused); + virtual bool _handle_key(const key key_press); + + void _search(const wchar_t ch); + void _move_to(const int index); + void _render_row(const int y, const std::vector& values, const bool fill); + void _render_selection(const bool selected); + + std::vector _widths; + std::vector _headers; + std::vector> _rows; + int _max_rows = 0; + int _selection = 0; + int _scroll = 0; +}; + +class dropdown_control : public control { +public: + dropdown_control(const std::wstring_view label, const std::vector& options, const int& label_space, layout& parent); + void options(const std::vector& options); + int selection() const; + void selection(const int index); + +private: + virtual int _min_width() const; + virtual void _reposition(const int row, const int col, const int height, const int width); + virtual void _instantiate(borders& borders); + virtual void _redraw(const bool focused); + virtual void _focus(const bool focused); + virtual bool _handle_key(const key key_press); + + void _search(const wchar_t ch); + void _move_to(const int index); + + const int& _label_space; + std::wstring _label; + std::vector _options; + int _label_width = 0; + int _selection = 0; +}; + +class button_control : public control { +public: + button_control(const std::wstring_view label, const int id, layout& parent); + +private: + virtual int _min_width() const; + virtual void _instantiate(borders& borders); + virtual void _focus(const bool focused); + virtual bool _handle_key(const key key_press); + + std::wstring _label; + int _id; +}; + +class layout : public control { +public: + enum class direction { + top_to_bottom, + left_to_right + }; + enum class alignment { + left, + top = left, + center, + right, + bottom = right, + fill + }; + + layout(dialog& dlg, layout& parent); + dialog& dlg(); + text_control& add_text(const std::wstring_view value); + input_control& add_input(const std::wstring_view label, const int width); + list_control& add_list(const std::initializer_list headers, const std::initializer_list widths, const int height); + dropdown_control& add_dropdown(const std::wstring_view label, const std::vector& options); + button_control& add_button(const std::wstring_view label, const int id, const bool is_default = false); + layout& add_group(const alignment halign = alignment::left); + void add_gap(); + +protected: + friend control; + + virtual int _min_height() const; + virtual int _min_width() const; + virtual void _reposition(const int row, const int col, const int height, const int width); + virtual void _instantiate(borders& borders); + virtual bool _handle_key(const key key_press); + + int _margin_top = 0; + int _margin_left = 0; + int _margin_bottom = 0; + int _margin_right = 0; + +private: + template + T& _add_control(Types&&... args); + void _track_label_width(const std::wstring_view label, const control& control); + static int _offset(const int available, const int used, const alignment align); + int _arrow_index() const; + + dialog& _dlg; + std::vector> _controls; + std::vector _arrow_order; + direction _direction = direction::top_to_bottom; + alignment _valign = alignment::center; + alignment _halign = alignment::center; + int _label_width = 0; + int _input_width = 0; +}; + +class dialog : public layout { +public: + static void initialize(const capabilities& caps); + + dialog(const std::wstring_view title); + int show(); + void close(const int id); + void focus(control& control); + const control& focus() const; + int max_width() const; + void on_validate(const std::function handler); + +private: + friend layout; + friend control; + + virtual bool _handle_key(const key key_press); + + int _tab_index() const; + void _focus_control(control* control); + void _dirty_control(control* control); + + std::wstring _title; + std::vector _tab_order; + control* _focused_control = nullptr; + control* _initial_focused_control = nullptr; + std::vector _dirty_controls; + std::optional _default_return_code; + std::optional _return_code; + std::function _validate_handler; + std::vector _borders; +}; diff --git a/src/font.cpp b/src/font.cpp new file mode 100644 index 0000000..d56c562 --- /dev/null +++ b/src/font.cpp @@ -0,0 +1,59 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "font.h" + +#include "vt.h" + +constexpr auto font_10x16 = R"(0;2;1;10;0;2;16;0{ @ +NNNNNNNNNN/??????????/??????????; +~~~~~~~~~~/~~~~~~~~~~/NNNNNNNNNN;;;; +?????????~/?????????~/GGGGGGGGGN;;;; +~~~~~~~~~~/~~~~~~~~~~/??????????; +~@@@@@@@@@/~?????????/N?????????; +??????????/??????????/GGGGGGGGGG; +@@@@@@@@@~/?????????~/?????????N;;; +??????????/??????????/MMMMMMMMMM; +??????????/oooooooooo/NNNNNNNNNN; +??????????/~~~~~~~~~~/NNNNNNNNNN; +wwwwwwwwww/~~~~~~~~~~/NNNNNNNNNN; +FFFFFFFFFF/??????????/??????????; +~~~~~~~~~~/??????????/??????????; +~~~~~~~~~~/NNNNNNNNNN/??????????; +~~~~~~~~~~/~~~~~~~~~~/@@@@@@@@@@;;;;; +@@@@@@@@@@/??????????/GGGGGGGGGG;;; +XTaCGXTaCG/eTGPaeTGPa/HDACGHDACG; +??????????/?????o{}~N/GKMENNNNN?; +___?_ooGG[/@_}~~~????/?NNNNNEEEE; +{{{wooWG??/??????????/EEE???????; +??????????/????__ooo_/?KA@@@@@@@; +BFKG~~~~~?/_??_~~~^N?/BBFEEEAAA@; +?~~~~~????/o~~^NB????/@?????????;;;;;;;;;;;;;;;;;;;;; +~?????????/~?????????/N?????????;; +?????????~/?????????~/?????????N; +~~~~~~~~~~/BBBBBBBBBB/??????????; +??????????/{{{{{{{{{{/NNNNNNNNNN; +~?????????/~?????????/NGGGGGGGGG;;;;;;;;;;;;;;;;;;;;;; +?????????~/??ACGCA??~/?????????N;;;;;;;; +@@@@@@@@@@/??????????/?????????? +)"; + +soft_font::soft_font() +{ + auto font_data = std::string{font_10x16}; + // Some terminals (like RLogin) will not cope with DECDLD content + // containing newlines, so we need to strip those out first. + std::erase(font_data, '\n'); + vtout.dcs(font_data); + // Designate the soft font as G1. + vtout.scs(1, " @"); +} + +soft_font::~soft_font() +{ + // Make sure the ASCII character set is restored on exit. + vtout.scs(1, "B"); + // Also erase the font buffers on exit. + vtout.dcs("0;0;2{ @"); +} diff --git a/src/font.h b/src/font.h new file mode 100644 index 0000000..b6ff21b --- /dev/null +++ b/src/font.h @@ -0,0 +1,11 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +class soft_font { +public: + soft_font(); + ~soft_font(); +}; diff --git a/src/glyphs.cpp b/src/glyphs.cpp new file mode 100644 index 0000000..acbd850 --- /dev/null +++ b/src/glyphs.cpp @@ -0,0 +1,489 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "glyphs.h" + +#include +#include + +namespace { + + template + void split(const std::string_view s, const char separator, const T&& callback) + { + auto start = 0; + for (;;) { + const auto slash = s.find(separator, start); + if (slash != std::string::npos) { + callback(s.substr(start, slash - start)); + start = slash + 1; + } else { + callback(s.substr(start)); + break; + } + } + } + + bool is_sixel_char(const char ch) + { + return ch >= '?' && ch <= '~'; + } + + bool is_non_blank_sixel_char(const char ch) + { + return ch >= '@' && ch <= '~'; + } + +} // namespace + +glyph::glyph(const std::string_view sixels) + : _sixels{sixels} +{ + split(_sixels, '/', [&](const auto sixel_row) { + _used_height += 6; + _used_width = std::max(_used_width, + std::count_if(sixel_row.begin(), sixel_row.end(), is_sixel_char)); + }); +} + +const std::string& glyph::str() const +{ + return _sixels; +} + +std::vector glyph::pixels(const int cell_width, const int cell_height) const +{ + auto pixels = std::vector(cell_width * cell_height, 0); + auto y = 0; + split(_sixels, '/', [&](const auto sixel_row) { + auto x = 0; + for (auto ch : sixel_row) + if (is_sixel_char(ch)) { + ch -= '?'; + if (x >= cell_width) break; + for (auto i = 0; i < 6; i++) { + if (y + i >= cell_height) break; + pixels[(y + i) * cell_width + x] = ch & 1; + ch >>= 1; + } + x++; + } + y += 6; + }); + return pixels; +} + +void glyph::pixels(const int cell_width, const int cell_height, const std::vector& pixels) +{ + // If there are any whitespace characters surrounding the original + // sixel content, we try and preserve that when updating the glyph. + const auto start = std::find_if(_sixels.begin(), _sixels.end(), is_sixel_char); + const auto end = std::find_if(_sixels.rbegin(), _sixels.rend(), is_sixel_char); + const auto prefix = _sixels.substr(0, start - _sixels.begin()); + const auto suffix = end != _sixels.rend() ? _sixels.substr(_sixels.rend() - end) : ""; + _sixels = prefix; + for (auto y = 0; y < cell_height; y += 6) { + if (y) _sixels += "/"; + for (auto x = 0; x < cell_width; x++) { + auto value = '?'; + for (auto i = 0; i < 6; i++) { + if (y + i >= cell_height) break; + if (pixels[(y + i) * cell_width + x]) value += 1 << i; + } + _sixels += value; + } + } + _sixels += suffix; +} + +bool glyph::used() const +{ + return std::count_if(_sixels.begin(), _sixels.end(), is_non_blank_sixel_char) > 0; +} + +glyph_reference::glyph_reference(glyph_manager& manager, const int index) + : _manager{manager}, _index{index} +{ +} + +glyph_reference& glyph_reference::operator=(const std::vector& pixels) +{ + _manager._glyph_pixels(_index, pixels); + return *this; +} + +glyph_reference::operator std::vector() const +{ + return _manager._glyph_pixels(_index); +} + +bool glyph_reference::used() const +{ + const auto internal_index = _index - _manager._first_index; + if (internal_index >= 0 && internal_index < _manager._glyphs.size()) + return _manager._glyphs[internal_index].used(); + else + return false; +} + +glyph_manager::glyph_manager() +{ + clear(); +} + +void glyph_manager::clear() +{ + clear({0, 0, 0, 10, 0, 2, 16, 0}, " @"); +} + +void glyph_manager::clear(const std::vector& params, const std::string_view id) +{ + c1_controls(false); + _prefix.clear(); + _suffix.clear(); + _id = id; + _sixel_prefix.clear(); + _sixel_suffix.clear(); + _parms = std::make_unique(params); + _glyphs.clear(); + _size = _parms->pcss().value_or(0) == 1 ? 96 : 94; + _first_index = _parms->pcn().value_or(_size == 96 ? 0 : 1); + std::tie(_cell_width, _cell_height, _pixel_aspect_ratio) = _detect_dimensions(); +} + +bool glyph_manager::load(const std::filesystem::path& path) +{ + auto file = std::ifstream{path, std::ios::binary}; + const auto size = file_size(path); + auto contents = std::string(size, '\0'); + file.read(contents.data(), size); + + const auto pattern = R"EGEX((\x1BP|\x90|R"\()([\d\s;]*)\{([\s!-/]*[0-~])(\s*)([\s/;?-~]+?)(\s*)(\x1B|\x9C|\)";))EGEX"; + auto match = std::smatch{}; + if (std::regex_search(contents, match, std::regex(pattern))) { + _prefix = match.prefix().str(); + _suffix = match.suffix().str(); + _introducer = match[1].str(); + _parms = std::make_unique(match[2].str()); + _id = match[3].str(); + _sixel_prefix = match[4].str(); + _sixel_suffix = match[6].str(); + _terminator = match[7].str(); + _glyphs.clear(); + split(match[5].str(), ';', [&](const auto glyph_sixels) { + _glyphs.emplace_back(glyph_sixels); + }); + _size = _parms->pcss().value_or(0) == 1 ? 96 : 94; + _first_index = _parms->pcn().value_or(_size == 96 ? 0 : 1); + std::tie(_cell_width, _cell_height, _pixel_aspect_ratio) = _detect_dimensions(); + return true; + } + return false; +} + +bool glyph_manager::save(const std::filesystem::path& path) +{ + auto contents = _prefix; + contents += _introducer; + contents += _parms->str(); + contents += "{"; + contents += _id; + contents += _sixel_prefix; + + for (auto i = 0; i < _glyphs.size(); i++) { + if (i) contents += ";"; + contents += _glyphs[i].str(); + } + + contents += _sixel_suffix; + contents += _terminator; + contents += _suffix; + + auto file = std::ofstream{path, std::ios::binary}; + file << contents; + + return true; +} + +glyph_manager::parameters& glyph_manager::params() +{ + return *_parms; +} + +const glyph_manager::parameters& glyph_manager::params() const +{ + return *_parms; +} + +const std::string& glyph_manager::id() const +{ + return _id; +} + +void glyph_manager::id(const std::string_view id) +{ + _id = id; +} + +const std::optional glyph_manager::c1_controls() const +{ + if (_introducer == "\x90") + return true; + else if (_introducer == "\x1BP") + return false; + else + return {}; +} + +void glyph_manager::c1_controls(const bool c1_8bit) +{ + if (c1_8bit) { + _introducer = "\x90"; + _terminator = "\x9C"; + } else { + _introducer = "\x1BP"; + _terminator = "\x1B\\"; + } +} + +int glyph_manager::size() const +{ + return _size; +} + +int glyph_manager::first_used() const +{ + return _first_index; +} + +int glyph_manager::cell_width() const +{ + return _cell_width; +} + +int glyph_manager::cell_height() const +{ + return _cell_height; +} + +int glyph_manager::pixel_aspect_ratio() const +{ + return _pixel_aspect_ratio; +} + +glyph_reference glyph_manager::operator[](const int index) +{ + return {*this, index}; +} + +std::vector glyph_manager::_glyph_pixels(const int index) +{ + const auto internal_index = index - _first_index; + if (internal_index < 0 || internal_index >= _glyphs.size()) + return std::vector(_cell_width * _cell_height, 0); + else + return _glyphs[internal_index].pixels(_cell_width, _cell_height); +} + +void glyph_manager::_glyph_pixels(const int index, const std::vector& pixels) +{ + while (index < _first_index) { + _glyphs.insert(_glyphs.begin(), glyph{""}); + _parms->pcn(--_first_index); + } + const auto internal_index = index - _first_index; + while (internal_index >= _glyphs.size()) + _glyphs.emplace_back(""); + _glyphs[internal_index].pixels(_cell_width, _cell_height, pixels); +} + +std::tuple glyph_manager::_detect_dimensions() +{ + auto screen_properties = std::make_tuple(80, 24, 200); + switch (_parms->pss().value_or(0)) { + case 2: screen_properties = std::make_tuple(132, 24, 334); break; + case 11: screen_properties = std::make_tuple(80, 36, 125); break; + case 12: screen_properties = std::make_tuple(132, 36, 209); break; + case 21: screen_properties = std::make_tuple(80, 48, 100); break; + case 22: screen_properties = std::make_tuple(132, 48, 167); break; + } + const auto [cpp, lpp, cell_aspect_ratio] = screen_properties; + auto declared_width = _parms->pcmw().value_or(0); + auto declared_height = _parms->pcmh().value_or(0); + auto declared_as_matrix = declared_width >= 2 && declared_width <= 4; + if (declared_as_matrix) { + // If size declared as a matrix, it's assumed to be targetting a VT2xx + // with a 2:1 pixel AR. The cell size is 8x10, 6x10, or 5x10, for matrix + // values 4, 3, and 2, although 80 column mode is always 8x10. + if (cpp == 80 || declared_width == 4) + return {8, 10, 200}; + else if (declared_width == 3) + return {6, 10, 200}; + else + return {5, 10, 200}; + } + const auto text_usage = _parms->pu() != 2; + const auto text_adjust = [=](const auto full_width) { + if (text_usage && declared_width) + return std::min(declared_width, full_width); + else + return full_width; + }; + if (lpp != 24) { + // If LPP isn't 24, assume VT420/VT5xx with 1.25:1 pixel AR. + const auto cell_width = cpp == 132 ? 6 : 10; + const auto cell_height = lpp == 48 ? 8 : 10; + if (declared_width <= cell_width && declared_height <= cell_height) + return {text_adjust(cell_width), cell_height, 125}; + } + if (declared_width && declared_height && !text_usage) { + // If size is explicit, calculate the pixel AR, relative to the cell AR. + const auto pixel_aspect_ratio = declared_width * cell_aspect_ratio / declared_height; + return {declared_width, declared_height, pixel_aspect_ratio}; + } + auto used_width = 0, used_height = 0; + for (auto glyph : _glyphs) { + used_width = std::max(used_width, glyph._used_width); + used_height = std::max(used_height, glyph._used_height); + } + const auto in_range = [=](const auto cell_width, const auto cell_height) { + const auto sixel_height = (cell_height + 5) / 6 * 6; + const auto height_in_range = declared_height ? declared_height <= cell_height : used_height <= sixel_height; + const auto width_in_range = declared_width ? declared_width <= cell_width : used_width <= cell_width; + return height_in_range && width_in_range; + }; + const auto unspecified_size = declared_width == 0 && declared_height == 0; + if (cpp == 80) { + if (in_range(8, 10) && unspecified_size) + return {8, 10, 200}; // VT2xx, 2:1 pixel AR + else if (in_range(15, 12)) + return {text_adjust(15), 12, 250}; // VT320, 2.5:1 pixel AR + else if (in_range(10, 16)) + return {text_adjust(10), 16, 125}; // VT420 & VT5xx, 1.25:1 pixel AR + else if (in_range(10, 20)) + return {text_adjust(10), 20, 100}; // VT340, 1:1 pixel AR + else if (in_range(12, 30)) + return {text_adjust(12), 30, 80}; // VT382, 0.8:1 pixel AR + else + return {text_adjust(max_width), max_height, 100}; + } else { + if (in_range(6, 10) && unspecified_size) + return {6, 10, 200}; // VT240, 2:1 AR + else if (in_range(9, 12)) + return {text_adjust(9), 12, 250}; // VT320, 2.5:1 pixel AR + else if (in_range(6, 16)) + return {text_adjust(6), 16, 125}; // VT420 & VT5xx, 1.25:1 pixel AR + else if (in_range(6, 20)) + return {text_adjust(6), 20, 100}; // VT340, 1:1 pixel AR + else if (in_range(7, 30)) + return {text_adjust(7), 30, 80}; // VT382, 0.8:1 pixel AR + else + return {text_adjust(max_width), max_height, 100}; + } +} + +glyph_manager::parameters::parameters(const std::string_view str) + : _str{str} +{ + auto value = std::optional{}; + for (auto ch : str) { + if (ch >= '0' && ch <= '9') + value = value.value_or(0) * 10 + (ch - '0'); + else if (ch == ';') { + _values.push_back(value); + value = {}; + } + } + _values.push_back(value); + _values_used = _values.size(); + while (_values.size() < 8) + _values.push_back({}); +} + +glyph_manager::parameters::parameters(const std::vector& values) +{ + for (auto value : values) + _values.emplace_back(value); + _values_used = _values.size(); + while (_values.size() < 8) + _values.push_back({}); + _rebuild(); +} + +const std::string& glyph_manager::parameters::str() const +{ + return _str; +} + +std::optional glyph_manager::parameters::pfn() const +{ + return _values[0]; +} + +void glyph_manager::parameters::pfn(const std::optional value) +{ + _values[0] = value; + _rebuild(); +} + +std::optional glyph_manager::parameters::pcn() const +{ + return _values[1]; +} + +void glyph_manager::parameters::pcn(const std::optional value) +{ + _values[1] = value; + _rebuild(); +} + +std::optional glyph_manager::parameters::pe() const +{ + return _values[2]; +} + +void glyph_manager::parameters::pe(const std::optional value) +{ + _values[2] = value; + _rebuild(); +} + +std::optional glyph_manager::parameters::pcmw() const +{ + return _values[3]; +} + +std::optional glyph_manager::parameters::pss() const +{ + return _values[4]; +} + +std::optional glyph_manager::parameters::pu() const +{ + return _values[5]; +} + +std::optional glyph_manager::parameters::pcmh() const +{ + return _values[6]; +} + +std::optional glyph_manager::parameters::pcss() const +{ + return _values[7]; +} + +void glyph_manager::parameters::_rebuild() +{ + const auto last = std::find_if(_values.rbegin(), _values.rend(), [](const auto value) { + return value.has_value(); + }); + _values_used = std::max(_values_used, std::distance(last, _values.rend())); + auto str = std::string{}; + for (auto i = 0; i < _values_used; i++) { + if (i > 0) str += ";"; + if (_values[i]) str += std::to_string(_values[i].value()); + } + _str = str; +} diff --git a/src/glyphs.h b/src/glyphs.h new file mode 100644 index 0000000..85510cd --- /dev/null +++ b/src/glyphs.h @@ -0,0 +1,114 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class glyph_manager; + +class glyph { +public: + glyph(const std::string_view sixels); + const std::string& str() const; + std::vector pixels(const int cell_width, const int cell_height) const; + void pixels(const int cell_width, const int cell_height, const std::vector& pixels); + bool used() const; + +private: + friend glyph_manager; + std::string _sixels; + int _used_width = 0; + int _used_height = 0; +}; + +class glyph_reference { +public: + glyph_reference(glyph_manager& manager, const int index); + glyph_reference& operator=(const std::vector& pixels); + operator std::vector() const; + bool used() const; + +private: + glyph_manager& _manager; + int _index; +}; + +class glyph_manager { +public: + class parameters; + + glyph_manager(); + void clear(); + void clear(const std::vector& params, const std::string_view id); + bool load(const std::filesystem::path& path); + bool save(const std::filesystem::path& path); + parameters& params(); + const parameters& params() const; + const std::string& id() const; + void id(const std::string_view id); + const std::optional c1_controls() const; + void c1_controls(const bool c1_8bit); + int size() const; + int first_used() const; + int cell_width() const; + int cell_height() const; + int pixel_aspect_ratio() const; + glyph_reference operator[](const int index); + +private: + friend glyph_reference; + + std::vector _glyph_pixels(const int index); + void _glyph_pixels(const int _index, const std::vector& pixels); + std::tuple _detect_dimensions(); + + static constexpr int max_width = 16; + static constexpr int max_height = 32; + std::string _prefix; + std::string _suffix; + std::string _introducer; + std::string _terminator; + std::string _id; + std::string _sixel_prefix; + std::string _sixel_suffix; + std::unique_ptr _parms; + std::vector _glyphs; + int _size; + int _first_index; + int _cell_width; + int _cell_height; + int _pixel_aspect_ratio; +}; + +class glyph_manager::parameters { +public: + parameters(const std::string_view str); + parameters(const std::vector& values); + const std::string& str() const; + std::optional pfn() const; + void pfn(const std::optional value); + std::optional pcn() const; + void pcn(const std::optional value); + std::optional pe() const; + void pe(const std::optional value); + std::optional pcmw() const; + std::optional pss() const; + std::optional pu() const; + std::optional pcmh() const; + std::optional pcss() const; + +private: + void _rebuild(); + + std::string _str; + std::vector> _values; + int _values_used = 0; +}; diff --git a/src/iso2022.cpp b/src/iso2022.cpp new file mode 100644 index 0000000..6fdc3f3 --- /dev/null +++ b/src/iso2022.cpp @@ -0,0 +1,93 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "iso2022.h" + +#include "charsets.h" +#include "vt.h" + +#include +#include + +namespace { + + constexpr int soft_font = 99; + + std::unordered_map build_charset_map() + { + auto charset_map = std::unordered_map{}; + for (auto i = 0; i < charset::all.size(); i++) { + const auto& cs = charset::all[i]; + const auto& glyphs = cs.glyphs(); + const auto base = cs.size() == 94 ? '!' : ' '; + for (auto j = 0; j < glyphs.length(); j++) { + const auto wide_char = glyphs[j]; + auto& ref = charset_map[wide_char]; + if (ref == 0) + ref = base + j + (i << 8); + } + } + for (auto j = '!'; j <= '~'; j++) { + const auto wide_char = L'\uE000' + j; + auto& ref = charset_map[wide_char]; + ref = j + (soft_font << 8); + } + return charset_map; + } + +} // namespace + +iso2022::iso2022(const wchar_t c) + : _s{&c, 1} +{ +} + +iso2022::iso2022(const std::wstring_view s) + : _s{s} +{ +} + +void iso2022::write(vt_stream& stream) const +{ + static auto charset_map = build_charset_map(); + static auto last_cs = -1; + auto last_gset = 0; + auto locking_shift = [&](auto gset) { + if (last_gset != gset) { + last_gset = gset; + switch (gset) { + case 0: stream.ls0(); break; + case 1: stream.ls1(); break; + case 2: stream.ls2(); break; + case 3: stream.ls3(); break; + } + } + }; + for (auto wch : _s) { + if (wch < ' ') { + stream.write(char(wch)); + } else if (wch >= ' ' && wch <= '~') { + locking_shift(0); + stream.write(char(wch)); + } else if (auto search = charset_map.find(wch); search != charset_map.end()) { + const auto ch = char(search->second & 0xFF); + const auto cs = search->second >> 8; + if (last_cs != cs) { + last_cs = cs; + if (cs == soft_font) + stream.scs(2, " @"); + else if (charset::all[cs].size() == 94) + stream.scs(2, charset::all[cs].id()); + else + stream.scs96(2, charset::all[cs].id()); + } + locking_shift(2); + stream.write(ch); + } else { + locking_shift(0); + stream.write('?'); + } + } + locking_shift(0); +} diff --git a/src/iso2022.h b/src/iso2022.h new file mode 100644 index 0000000..2a781c6 --- /dev/null +++ b/src/iso2022.h @@ -0,0 +1,20 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include +#include + +class vt_stream; + +class iso2022 { +public: + iso2022(const wchar_t c); + iso2022(const std::wstring_view s); + void write(vt_stream& stream) const; + +private: + std::wstring _s; +}; diff --git a/src/keyboard.cpp b/src/keyboard.cpp new file mode 100644 index 0000000..8afbf5a --- /dev/null +++ b/src/keyboard.cpp @@ -0,0 +1,196 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "keyboard.h" + +#include "capabilities.h" +#include "os.h" +#include "vt.h" + +#include + +namespace { + + key make_key(const key base, const int offset) + { + return static_cast(static_cast(base) + offset); + } + + key remove_modifier(const key base, const key modifier) + { + return static_cast(static_cast(base) & ~static_cast(modifier)); + } + + bool has_modifier(const key base, const key modifier) + { + return (static_cast(base) & static_cast(modifier)) != 0; + } + + key make_modifier(const std::vector& parms) + { + auto modifier = key::unmodified; + if (parms.size() >= 2) { + const auto modifier_parm = parms[1] - 1; + if (modifier_parm > 0) { + if (modifier_parm & 1) + modifier = modifier + key::shift; + if (modifier_parm & 2) + modifier = modifier + key::alt; + if (modifier_parm & 4) + modifier = modifier + key::ctrl; + } + } + return modifier; + } + + bool has_pc_keyboard = true; + + std::string key_label(const std::string pc_label, const std::string vt_label) + { + return has_pc_keyboard ? pc_label : vt_label; + } + +} // namespace + +void keyboard::initialize(const capabilities& caps) +{ + has_pc_keyboard = caps.has_pc_keyboard; +} + +key keyboard::read() +{ + enum { + ground, + esc, + csi, + ss3 + } state = ground; + auto parm = 0; + auto parm_list = std::vector{}; + vtout.flush(); + for (;;) { + const auto ch = os::getch(); + switch (state) { + case ground: + parm = 0; + parm_list.clear(); + switch (ch) { + case '\177': return key::bksp; + case '\b': return key::bksp; + case '\t': return key::tab; + case '\r': return key::enter; + case '\n': return key::enter; + case ' ': return key::space; + case '\033': state = esc; break; + } + if (ch >= 1 && ch <= 26) + return make_key(key::ctrl + key::a, ch - 1); + if (ch >= 'A' && ch <= 'Z') + return make_key(key::shift + key::a, ch - 'A'); + if (ch >= ' ' && ch < 127) + return make_key(key::space, ch - ' '); + break; + case esc: + if (ch >= 'a' && ch <= 'z') + return make_key(key::alt + key::a, ch - 'a'); + switch (ch) { + case '[': state = csi; break; + case 'O': state = ss3; break; + case '\033': state = esc; break; + default: state = ground; break; + } + break; + case csi: + if (ch >= '0' && ch <= '9') { + parm = parm * 10 + (ch - '0'); + } else if (ch == ';') { + parm_list.push_back(parm); + parm = 0; + } else { + state = ground; + parm_list.push_back(parm); + auto modifier = make_modifier(parm_list); + switch (ch) { + case 'Z': return key::shift + key::tab; + case 'A': return modifier + key::up; + case 'B': return modifier + key::down; + case 'C': return modifier + key::right; + case 'D': return modifier + key::left; + case 'H': return modifier + key::home; + case 'F': return modifier + key::end; + case '~': + switch (parm_list.front()) { + case 1: return modifier + key::home; // VT Find + case 2: return modifier + key::ins; // VT Insert Here + case 3: return modifier + key::del; // VT Remove + case 4: return modifier + key::end; // VT Select + case 5: return modifier + key::pgup; // VT Prev Screen + case 6: return modifier + key::pgdn; // VT Next Screen + case 7: return modifier + key::left; + case 8: return modifier + key::down; + case 9: return modifier + key::up; + case 10: return modifier + key::right; + case 11: return modifier + key::f1; + case 12: return modifier + key::f2; + case 13: return modifier + key::f3; + case 14: return modifier + key::f4; + case 15: return modifier + key::f5; + case 17: return modifier + key::f6; + case 18: return modifier + key::f7; + case 19: return modifier + key::f8; + case 20: return modifier + key::f9; + case 21: return modifier + key::f10; + case 28: return modifier + key::help; + } + break; + } + } + break; + case ss3: + state = ground; + switch (ch) { + case 'P': return key::pf1; + case 'Q': return key::pf2; + case 'R': return key::pf3; + case 'S': return key::pf4; + } + break; + } + } +} + +std::optional keyboard::printable(const key key_press) +{ + if (key_press >= key::space && key_press <= key::tilde) + return static_cast(key_press); + const auto unshifted_key = remove_modifier(key_press, key::shift); + if (unshifted_key >= key::a && unshifted_key <= key::z) + return static_cast(unshifted_key) + ('A' - 'a'); + return {}; +} + +std::string keyboard::to_string(const key key_press) +{ + using namespace std::string_literals; + auto modifiers = ""s; + if (has_modifier(key_press, key::ctrl)) modifiers += "Ctrl+"; + if (has_modifier(key_press, key::alt)) modifiers += "Alt+"; + if (has_modifier(key_press, key::shift)) modifiers += "Shift+"; + const auto k = remove_modifier(key_press, key::ctrl + key::alt + key::shift); + if (k == key::pgup) + return modifiers + key_label("PgUp", "Prev"); + if (k == key::pgdn) + return modifiers + key_label("PgDn", "Next"); + if (k == key::del) + return modifiers + key_label("Del", "Remove"); + if (k == key::tab) + return modifiers + "Tab"s; + if (k >= key::pf1 && k <= key::pf4) + return modifiers + key_label("F", "PF") + std::to_string(k - key::pf1 + 1); + if (k >= key::f1 && k <= key::f10) + return modifiers + "F"s + std::to_string(k - key::f1 + 1); + if (k >= key::a && k <= key::z) + return modifiers + static_cast('A' + (k - key::a)); + return ""s; +} diff --git a/src/keyboard.h b/src/keyboard.h new file mode 100644 index 0000000..b79c3cb --- /dev/null +++ b/src/keyboard.h @@ -0,0 +1,97 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include +#include + +enum class key { + unmodified = 0, + + up, + down, + left, + right, + home, + end, + pgup, + pgdn, + + pf1, + pf2, + pf3, + pf4, + + f1, + f2, + f3, + f4, + f5, + f6, + f7, + f8, + f9, + f10, + help, + + enter, + bksp, + ins, + del, + tab, + space = ' ', + tilde = '~', + + a = 'a', + b, + c, + d, + e, + f, + g, + h, + i, + j, + k, + l, + m, + n, + o, + p, + q, + r, + s, + t, + u, + v, + w, + x, + y, + z, + + alt = 0x10000, + ctrl = 0x20000, + shift = 0x40000, +}; + +constexpr key operator+(const key left, const key right) +{ + return static_cast(static_cast(left) + static_cast(right)); +} + +constexpr int operator-(const key left, const key right) +{ + return static_cast(left) - static_cast(right); +} + +class capabilities; + +class keyboard { +public: + static void initialize(const capabilities& caps); + static key read(); + static std::optional printable(const key key_press); + static std::string to_string(const key key_press); +}; diff --git a/src/macros.cpp b/src/macros.cpp new file mode 100644 index 0000000..85f6c94 --- /dev/null +++ b/src/macros.cpp @@ -0,0 +1,71 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "macros.h" + +#include "vt.h" + +int macro_manager::reserve_id() +{ + static int _next_id = 0; + // Clear out all existing macros on first use. + if (_next_id == 0) vtout.decdmac({}, 1, {}); + return _next_id++; +} + +int macro_manager::create(macro_callback callback) +{ + return create(reserve_id(), callback); +} + +int macro_manager::create(const int id, macro_callback callback) +{ + auto buffer = macro_buffer{}; + auto buffer_stream = std::ostream(&buffer); + auto vt_stream = macro_stream{buffer, buffer_stream}; + callback(vt_stream); + vt_stream.flush(); + vtout.decdmac(id, {}, 1, buffer.encoded()); + return id; +} + +int macro_buffer::sync() +{ + const auto& text = str(); + _encoded.reserve(_encoded.size() + text.length() * 2); + for (auto i = 0, offset = 0; i < text.length(); i++) { + static constexpr auto hex = "0123456789ABCDEF"; + _encoded += hex[(text[i] >> 4) & 0x0F]; + _encoded += hex[text[i] & 0x0F]; + } + str(""); + return 0; +} + +const std::string& macro_buffer::encoded() const +{ + return _encoded; +} + +std::string& macro_buffer::encoded() +{ + return _encoded; +} + +macro_stream::macro_stream(macro_buffer& buffer, std::ostream& buffer_stream) + : vt_stream{buffer_stream}, _buffer{buffer} +{ +} + +void macro_stream::repeat(const int count, macro_callback callback) +{ + auto& encoded = _buffer.encoded(); + flush(); + encoded.push_back('!'); + encoded.append(std::to_string(count)); + encoded.push_back(';'); + callback(*this); + flush(); + encoded.push_back(';'); +} diff --git a/src/macros.h b/src/macros.h new file mode 100644 index 0000000..2cafabf --- /dev/null +++ b/src/macros.h @@ -0,0 +1,40 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include "vt.h" + +#include +#include +#include + +class macro_stream; +using macro_callback = std::function; + +class macro_manager { +public: + static int reserve_id(); + static int create(macro_callback callback); + static int create(const int id, macro_callback callback); +}; + +class macro_buffer : public std::stringbuf { +public: + virtual int sync(); + const std::string& encoded() const; + std::string& encoded(); + +private: + std::string _encoded; +}; + +class macro_stream : public vt_stream { +public: + macro_stream(macro_buffer& buffer, std::ostream& buffer_stream); + void repeat(const int count, macro_callback callback); + +private: + macro_buffer& _buffer; +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..05bacd0 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,105 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "application.h" +#include "capabilities.h" +#include "coloring.h" +#include "dialog.h" +#include "font.h" +#include "os.h" +#include "vt.h" + +bool check_compatibility(const capabilities& caps) +{ + const auto compatible = + caps.has_soft_fonts && + caps.has_horizontal_scrolling && + caps.has_color && + caps.has_rectangle_ops && + caps.has_macros && + caps.has_pages; + if (!compatible) { + vtout.write(application::name); + vtout.write(" requires a VT525-compatible terminal.\n"); + return false; + } + if (caps.height < 24) { + vtout.write(application::name); + vtout.write(" requires a minimum screen height of 24.\n"); + return false; + } + if (caps.width < 54) { + vtout.write(application::name); + vtout.write(" requires a minimum screen width of 54.\n"); + return false; + } + return true; +} + +int main(int argc, const char* argv[]) +{ + os os; + capabilities caps; + if (!check_compatibility(caps)) + return 1; + + // Set the window title. + vtout.decswt(application::name); + // Set default attributes. + vtout.sgr(); + // Clear the screen. + vtout.ed(2); + // Hide the cursor and disable auto wrap. + vtout.rm('?', {25, 7}); + // Enable horizontal margins and origin mode. + vtout.sm('?', {69, 6}); + // Enable rectangular change extent. + vtout.decsace(2); + + vtout.cup(caps.height / 2, (caps.width - 10) / 2 + 1); + vtout.write("Loading..."); + vtout.flush(); + + // Load the soft font. + const auto font = soft_font{}; + // Setup the color palette. + const auto colors = coloring{caps}; + + auto start_path = std::filesystem::path{}; + for (int i = 1; i < argc; i++) { + auto arg = std::string{argv[i]}; + if (!arg.starts_with("-")) { + start_path = arg; + break; + } + } + + dialog::initialize(caps); + keyboard::initialize(caps); + application app{caps, start_path}; + app.run(); + + // Disable horizontal margins and origin mode. + vtout.rm('?', {69, 6}); + // Clear the window title. + vtout.decswt(); + // Clean out our macros on exit. + vtout.decdmac({}, 1, {}); + // Set default attributes. + vtout.sgr({}); + // Clear all pages. + vtout.cup(); + vtout.ppa(3); + vtout.ed(); + vtout.ppa(2); + vtout.ed(); + vtout.ppa(1); + vtout.ed(); + // Show the cursor and reenable autowrap. + vtout.sm('?', {25, 7}); + // Restore default character set. + vtout.ls0(); + + return 0; +} diff --git a/src/menu.cpp b/src/menu.cpp new file mode 100644 index 0000000..78bcb3b --- /dev/null +++ b/src/menu.cpp @@ -0,0 +1,306 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "menu.h" + +#include "macros.h" + +namespace color { + + constexpr auto primary_init = {0, 1, 7, 37, 40}; // Black on BrightestWhite + constexpr auto primary_normal = {1, 37}; // BrightestWhite background + constexpr auto primary_focus = {22, 35}; // LighterBlue background + + constexpr auto secondary_init = {0, 1, 7, 36, 40}; // Black on BrightWhite + constexpr auto secondary_normal = {1, 36}; // BrightWhite background + constexpr auto secondary_focus = {22, 34}; // LightBlue background + constexpr auto secondary_disabled = {46}; // LightGray foreground + +} // namespace color + +namespace { + + std::string generate_accelerator_name(const std::optional accelerator) + { + if (accelerator) + return keyboard::to_string(accelerator.value()); + else + return ""; + } + + std::wstring markup_label(const std::wstring_view name) + { + const auto ampersand = name.find('&'); + if (ampersand == std::wstring::npos) + return std::wstring{name}; + else { + auto result = std::wstring{name.substr(0, ampersand)}; + result += L"\033[4m"; + result += name[ampersand + 1]; + result += L"\033[24m"; + result += name.substr(ampersand + 2); + return result; + } + } + + int calculate_shortcut(const std::wstring_view name) + { + const auto pos = name.find('&'); + if (pos != std::string::npos) { + const auto ch = std::toupper(name[pos + 1]); + if (ch >= 'A' && ch <= 'Z') return ch - 'A'; + } + return -1; + } + +} // namespace + +int menu_group::_close_macro = -1; + +menu_group::menu_group(const std::wstring_view name, const int xoffset, const std::unordered_set& disabled_ids) + : _name{name}, _left{xoffset}, _disabled_ids{disabled_ids} +{ + _open_macro = macro_manager::reserve_id(); + if (_close_macro == -1) { + _close_macro = macro_manager::create([](auto& macro) { + macro.deccra({}, {}, {}, {}, 2, {}, {}, 1); + macro.decstbm(); + macro.decslrm(); + macro.deccara({}, {}, 1, {}, color::primary_normal); + }); + } +} + +int menu_group::left() const +{ + return _left; +} + +int menu_group::right() const +{ + return _left + _name.length(); +} + +int menu_group::macro_id() const +{ + return _open_macro; +} + +void menu_group::add(const int entry_id, const std::wstring_view entry_name) +{ + _entry_ids.push_back(entry_id); + _shortcuts.emplace(calculate_shortcut(entry_name), entry_id); + _entry_count++; +} + +void menu_group::render() const +{ + vtout.cup(1, _left + 1); + vtout.write(markup_label(_name)); +} + +void menu_group::open() +{ + vtout.decinvm(_open_macro); + for (auto i = 0; i < _entry_ids.size(); i++) + if (_disabled(i)) + vtout.deccara(i + 1, {}, i + 1, {}, color::secondary_disabled); + _focus_index = 0; + while (_disabled(_focus_index)) + _focus_index++; + vtout.deccara(_focus_index + 1, {}, _focus_index + 1, {}, color::secondary_focus); +} + +void menu_group::close() +{ + vtout.decinvm(_close_macro); +} + +std::optional menu_group::process_key(const key keypress) +{ + switch (keypress) { + case key::up: + _move_focus(-1); + return {}; + case key::down: + _move_focus(+1); + return {}; + default: + if (auto id = _selection_for_key(keypress)) + if (!_disabled_ids.contains(id.value())) + return id; + return {}; + } +} + +bool menu_group::_disabled(const int entry_index) const +{ + return _disabled_ids.contains(_entry_ids[entry_index]); +} + +void menu_group::_move_focus(const int offset) +{ + vtout.deccara(_focus_index + 1, {}, _focus_index + 1, {}, color::secondary_normal); + do { + _focus_index = (_focus_index + offset + _entry_count) % _entry_count; + } while (_disabled(_focus_index)); + vtout.deccara(_focus_index + 1, {}, _focus_index + 1, {}, color::secondary_focus); +} + +std::optional menu_group::_selection_for_key(const key k) const +{ + if (k == key::enter) + return _entry_ids[_focus_index]; + if (k >= key::a && k <= key::z) + if (const auto match = _shortcuts.find(k - key::a); match != _shortcuts.end()) + return match->second; + if (k >= key::alt + key::a && k <= key::alt + key::z) + if (const auto match = _shortcuts.find(k - (key::alt + key::a)); match != _shortcuts.end()) + return match->second; + return {}; +} + +menu_builder::menu_builder(menu_group& group, std::unordered_map& accelerators) + : _group{group}, _accelerators{accelerators} +{ +} + +menu_builder::~menu_builder() +{ + macro_manager::create(_group.macro_id(), [&](auto& macro) { + macro.deccara({}, _group.left(), 1, _group.right(), color::primary_focus); + macro.decslrm(_group.left(), _group.left() + _width); + macro.decstbm(2, _entries.size() + 1); + macro.deccra({}, {}, {}, {}, 1, {}, {}, 2); + macro.sgr(color::secondary_init); + auto yoffset = 1; + for (auto& entry : _entries) { + const auto& [name, accelerator_name, has_separator] = entry; + macro.cup(yoffset++); + if (has_separator) macro.sgr({53}); + macro.write(' '); + macro.write(markup_label(name)); + macro.write_spaces(_width - name.length() - accelerator_name.length()); + macro.write(accelerator_name); + macro.write(' '); + if (has_separator) macro.sgr({55}); + } + }); +} + +void menu_builder::separator() +{ + _want_separator = true; +} + +void menu_builder::add(const int id, const std::wstring_view name, const std::optional accelerator, const std::optional accelerator2) +{ + const auto accelerator_name = generate_accelerator_name(accelerator); + const auto accelerator_width = !accelerator_name.empty() ? accelerator_name.length() + 3 : 0; + _group.add(id, name); + _width = std::max(_width, name.length() + accelerator_width); + _entries.emplace_back(name, accelerator_name, _want_separator); + if (accelerator) + _accelerators.emplace(accelerator.value(), id); + if (accelerator2) + _accelerators.emplace(accelerator2.value(), id); + _want_separator = false; +} + +menu_builder menu::add(const std::wstring_view name) +{ + const auto index = static_cast(_groups.size()); + const auto xoffset = _width_used + 2; + const auto& group = _groups.emplace_back(std::make_unique(name, xoffset, _disabled_ids)); + _shortcuts.emplace(calculate_shortcut(name), index); + _width_used += name.length() + 1; + return {*group, _accelerators}; +} + +void menu::render() const +{ + vtout.deccara({}, {}, 1, {}, color::primary_init); + vtout.sgr(color::primary_init); + vtout.cup(); + for (const auto& group : _groups) + group->render(); +} + +void menu::enable(const int entry_id, const bool enabled) +{ + if (enabled) + _disabled_ids.erase(entry_id); + else + _disabled_ids.insert(entry_id); +} + +std::optional menu::process_key(const key keypress) +{ + if (keypress == key::f10) + _open_group(0); + else if (auto group_index = _group_for_key(keypress)) + _open_group(group_index); + else if (auto selection = _selection_for_key(keypress)) + return selection; + else + return {}; + auto selection = std::optional{}; + auto width = _groups.size(); + while (!selection.has_value()) { + const auto k = keyboard::read(); + switch (k) { + case key::right: + _open_group(_index() + 1); + break; + case key::left: + _open_group(_index() - 1); + break; + case key::f10: + case key::bksp: + selection = -1; + break; + default: + if (auto group_index = _group_for_key(k)) + _open_group(group_index); + else + selection = _groups[_index()]->process_key(k); + break; + } + } + _open_group({}); + return selection; +} + +std::optional menu::_selection_for_key(const key k) const +{ + if (const auto match = _accelerators.find(k); match != _accelerators.end()) + if (!_disabled_ids.contains(match->second)) + return match->second; + return {}; +} + +std::optional menu::_group_for_key(const key k) const +{ + if (k >= key::alt + key::a && k <= key::alt + key::z) + if (const auto match = _shortcuts.find(k - (key::alt + key::a)); match != _shortcuts.end()) + return match->second; + return {}; +} + +void menu::_open_group(const std::optional new_index) +{ + if (_open_index) + _groups[_index()]->close(); + if (new_index) + _open_index = int((new_index.value() + _groups.size()) % _groups.size()); + else + _open_index = {}; + if (_open_index) + _groups[_index()]->open(); +} + +int menu::_index() const +{ + return _open_index.value(); +} diff --git a/src/menu.h b/src/menu.h new file mode 100644 index 0000000..dd7d10d --- /dev/null +++ b/src/menu.h @@ -0,0 +1,80 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include "keyboard.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +class menu_group { +public: + menu_group(const std::wstring_view name, const int xoffset, const std::unordered_set& disabled_ids); + int left() const; + int right() const; + int macro_id() const; + void add(const int entry_id, const std::wstring_view entry_name); + void render() const; + void open(); + void close(); + std::optional process_key(const key keypress); + +private: + bool _disabled(const int entry_index) const; + void _move_focus(const int offset); + std::optional _selection_for_key(const key k) const; + + std::wstring _name; + int _left; + const std::unordered_set& _disabled_ids; + std::vector _entry_ids; + std::unordered_map _shortcuts; + int _entry_count = 0; + int _focus_index = 0; + int _open_macro; + static int _close_macro; +}; + +class menu_builder { +public: + menu_builder(menu_group& group, std::unordered_map& accelerators); + ~menu_builder(); + void separator(); + void add(const int id, const std::wstring_view name, const std::optional accelerator = {}, const std::optional accelerator2 = {}); + +private: + menu_group& _group; + std::unordered_map& _accelerators; + std::vector> _entries; + int _width = 0; + bool _want_separator = false; +}; + +class menu { +public: + menu_builder add(const std::wstring_view name); + void render() const; + void enable(const int entry_id, const bool enabled = true); + std::optional process_key(const key keypress); + +private: + std::optional _selection_for_key(const key k) const; + std::optional _group_for_key(const key k) const; + void _open_group(const std::optional new_index); + int _index() const; + + int _width_used = 0; + std::vector> _groups; + std::unordered_map _shortcuts; + std::unordered_map _accelerators; + std::unordered_set _disabled_ids; + std::optional _open_index; +}; diff --git a/src/os.cpp b/src/os.cpp new file mode 100644 index 0000000..2c5bbe5 --- /dev/null +++ b/src/os.cpp @@ -0,0 +1,86 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "os.h" + +#ifdef _WIN32 + +#include + +DWORD output_mode; +DWORD input_mode; + +os::os() +{ + HANDLE output_handle = GetStdHandle(STD_OUTPUT_HANDLE); + GetConsoleMode(output_handle, &output_mode); + SetConsoleMode(output_handle, output_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN); + HANDLE input_handle = GetStdHandle(STD_INPUT_HANDLE); + GetConsoleMode(input_handle, &input_mode); + SetConsoleMode(input_handle, input_mode & ~ENABLE_LINE_INPUT & ~ENABLE_ECHO_INPUT & ~ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT); +} + +os::~os() +{ + HANDLE output_handle = GetStdHandle(STD_OUTPUT_HANDLE); + SetConsoleMode(output_handle, output_mode); + HANDLE input_handle = GetStdHandle(STD_INPUT_HANDLE); + SetConsoleMode(input_handle, input_mode); +} + +int os::getch() +{ + char ch; + DWORD chars_read = 0; + HANDLE input_handle = GetStdHandle(STD_INPUT_HANDLE); + ReadConsoleA(input_handle, &ch, 1, &chars_read, NULL); + return chars_read == 1 ? static_cast(ch) : -1; +} + +bool os::is_file_hidden(const std::filesystem::path& filepath) +{ + auto attrs = WIN32_FILE_ATTRIBUTE_DATA{}; + if (GetFileAttributesExW(filepath.c_str(), GetFileExInfoStandard, &attrs)) + return attrs.dwFileAttributes & (FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM); + else + return true; +} + +#endif + +#ifdef __linux__ + +#include +#include +#include + +#include + +struct termios term_attributes; + +os::os() +{ + tcgetattr(STDIN_FILENO, &term_attributes); + auto new_term_attributes = term_attributes; + new_term_attributes.c_lflag &= ~(ICANON | ISIG | ECHO | IEXTEN); + new_term_attributes.c_iflag &= ~(IXON); + tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_term_attributes); +} + +os::~os() +{ + tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_attributes); +} + +int os::getch() +{ + return getchar(); +} + +bool os::is_file_hidden(const std::filesystem::path& filepath) +{ + return *filepath.c_str() == '.'; +} + +#endif diff --git a/src/os.h b/src/os.h new file mode 100644 index 0000000..d978dda --- /dev/null +++ b/src/os.h @@ -0,0 +1,15 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include + +class os { +public: + os(); + ~os(); + static int getch(); + static bool is_file_hidden(const std::filesystem::path& filepath); +}; diff --git a/src/status.cpp b/src/status.cpp new file mode 100644 index 0000000..b595b7c --- /dev/null +++ b/src/status.cpp @@ -0,0 +1,106 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "status.h" + +#include "capabilities.h" +#include "charsets.h" +#include "vt.h" + +#include + +namespace { + + static constexpr auto status_color = {0, 1, 36, 42}; // BrightWhite on DarkerBlue + +} // namespace + +status::status(const capabilities& caps) + : _width{caps.width}, _height{caps.height} +{ + character_set("B", 94); +} + +void status::render() +{ + vtout.deccara(_height, {}, {}, {}, status_color); +} + +const std::wstring& status::filename() const +{ + return _filename; +} + +void status::filename(const std::wstring_view filename) +{ + const auto pad = 1 + int(_filename.length()) - int(filename.length()); + _filename = filename; + vtout.sgr(status_color); + vtout.cup(_height, 2); + vtout.write(_filename); + vtout.write_spaces(pad); +} + +void status::character_set(const std::string_view id, const int size) +{ + const auto set_values = [&](auto& cs) { + _char_index = -1; + _char_values = cs.glyphs(); + if (cs.size() == 94) { + _char_values.insert(_char_values.begin(), L' '); + _char_values.push_back(L'\177'); + } + }; + for (auto& cs : charset::all) { + if (cs.size() == size && cs.id() == id && !cs.glyphs().empty()) { + set_values(cs); + return; + } + } + // If no match, use ASCII + set_values(charset::all[2]); +} + +void status::index(const int index) +{ + using namespace std::string_literals; + if (_char_index != index) { + _char_index = index; + + vtout.sgr(status_color); + vtout.cup(_height, _width - 8); + + const auto ch = _char_values[index]; + if (ch == L'\177') + vtout.write("DEL"); + else if (ch == L' ' || ch == L'\240') + vtout.write(" SP"); + else { + vtout.write(" "); + vtout.write(ch); + } + + vtout.write(std::format(" 0x{:02X}", 0x20 + index)); + } +} + +int status::index() const +{ + return _char_index; +} + +void status::dirty(const bool dirty) +{ + if (_dirty != dirty) { + _dirty = dirty; + vtout.sgr(status_color); + vtout.cup(_height, 2 + int(_filename.length())); + vtout.write(_dirty ? "*" : " "); + } +} + +bool status::dirty() const +{ + return _dirty; +} diff --git a/src/status.h b/src/status.h new file mode 100644 index 0000000..44f751b --- /dev/null +++ b/src/status.h @@ -0,0 +1,31 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include +#include + +class capabilities; + +class status { +public: + status(const capabilities& caps); + void render(); + const std::wstring& filename() const; + void filename(const std::wstring_view filename); + void character_set(const std::string_view id, const int size); + void index(const int index); + int index() const; + void dirty(const bool dirty); + bool dirty() const; + +private: + int _width; + int _height; + std::wstring _filename; + bool _dirty = false; + int _char_index; + std::wstring _char_values; +}; diff --git a/src/vt.cpp b/src/vt.cpp new file mode 100644 index 0000000..d75f378 --- /dev/null +++ b/src/vt.cpp @@ -0,0 +1,388 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "vt.h" + +#include "iso2022.h" + +vt_stream vtout{std::cout}; + +vt_stream::vt_stream(std::ostream& stream) + : _stream{stream} +{ +} + +vt_stream::~vt_stream() +{ + flush(); +} + +void vt_stream::ls0() +{ + _char('\017'); +} + +void vt_stream::ls1() +{ + _char('\016'); +} + +void vt_stream::ls2() +{ + _string("\033n"); +} + +void vt_stream::ls3() +{ + _string("\033o"); +} + +void vt_stream::cup(const vt_parm row, const vt_parm col) +{ + _csi(); + _parms({row, col}); + _final("H"); +} + +void vt_stream::cuf(const vt_parm cols) +{ + _csi(); + _parm(cols); + _final("C"); +} + +void vt_stream::cub(const vt_parm cols) +{ + _csi(); + _parm(cols); + _final("D"); +} + +void vt_stream::ed(const vt_parm type) +{ + _csi(); + _parm(type); + _final("J"); +} + +void vt_stream::il(const vt_parm count) +{ + _csi(); + _parm(count); + _final("L"); +} + +void vt_stream::ppa(const vt_parm page) +{ + _csi(); + _parm(page); + _final(" P"); +} + +void vt_stream::sgr(const vt_parms attrs) +{ + _csi(); + _parms(attrs); + _final("m"); +} + +void vt_stream::sm(const vt_parm mode) +{ + sm({mode}); +} + +void vt_stream::rm(const vt_parm mode) +{ + rm({mode}); +} + +void vt_stream::sm(const char prefix, const vt_parm mode) +{ + sm(prefix, {mode}); +} + +void vt_stream::rm(const char prefix, const vt_parm mode) +{ + rm(prefix, {mode}); +} + +void vt_stream::sm(const vt_parms modes) +{ + _csi(); + _parms(modes); + _final("h"); +} + +void vt_stream::rm(const vt_parms modes) +{ + _csi(); + _parms(modes); + _final("l"); +} + +void vt_stream::sm(const char prefix, const vt_parms modes) +{ + _csi(); + _char(prefix); + _parms(modes); + _final("h"); +} + +void vt_stream::rm(const char prefix, const vt_parms modes) +{ + _csi(); + _char(prefix); + _parms(modes); + _final("l"); +} + +void vt_stream::dsr(const vt_parm id) +{ + _csi(); + _parm(id); + _final("n"); +} + +void vt_stream::dsr(const char prefix, const vt_parm id) +{ + _csi(); + _char(prefix); + _parm(id); + _final("n"); +} + +void vt_stream::scs(const int gset, const std::string_view id) +{ + _char('\033'); + _char("()*+"[gset]); + _string(id); +} + +void vt_stream::scs96(const int gset, const std::string_view id) +{ + _char('\033'); + _char(",-./"[gset]); + _string(id); +} + +void vt_stream::da() +{ + _string("\033[c"); +} + +void vt_stream::s7c1t() +{ + _string("\033 F"); +} + +void vt_stream::decsc() +{ + _string("\0337"); +} + +void vt_stream::decrc() +{ + _string("\0338"); +} + +void vt_stream::decfi() +{ + _string("\0339"); +} + +void vt_stream::decic(const vt_parm count) +{ + _csi(); + _parm(count); + _final("'}"); +} + +void vt_stream::decsace(const vt_parm extent) +{ + _csi(); + _parm(extent); + _final("*x"); +} + +void vt_stream::decrqm(const char prefix, const vt_parm mode) +{ + _csi(); + _char(prefix); + _parm(mode); + _final("$p"); +} + +void vt_stream::decac(const vt_parm a, const vt_parm b, const vt_parm c) +{ + _csi(); + _parms({a, b, c}); + _final(",|"); +} + +void vt_stream::decctr(const vt_parm type) +{ + _csi(); + _parms({2, type}); + _final("$u"); +} + +void vt_stream::decstbm(const vt_parm top, const vt_parm bottom) +{ + _csi(); + _parms({top, bottom}); + _final("r"); +} + +void vt_stream::decslrm(const vt_parm left, const vt_parm right) +{ + _csi(); + _parms({left, right}); + _final("s"); +} + +void vt_stream::decfra(const vt_parm ch, const vt_parm top, const vt_parm left, const vt_parm bottom, const vt_parm right) +{ + _csi(); + _parms({ch, top, left, bottom, right}); + _final("$x"); +} + +void vt_stream::deccra(const vt_parm top, const vt_parm left, const vt_parm bottom, const vt_parm right, const vt_parm dtop, const vt_parm dleft) +{ + _csi(); + _parms({top, left, bottom, right, {}, dtop, dleft, {}}); + _final("$v"); +} + +void vt_stream::deccra(const vt_parm top, const vt_parm left, const vt_parm bottom, const vt_parm right, const vt_parm page, const vt_parm dtop, const vt_parm dleft, const vt_parm dpage) +{ + _csi(); + _parms({top, left, bottom, right, page, dtop, dleft, dpage}); + _final("$v"); +} + +void vt_stream::deccara(const vt_parm top, const vt_parm left, const vt_parm bottom, const vt_parm right, const vt_parms attrs) +{ + _csi(); + _parms({top, left, bottom, right}, false); + _char(';'); + _parms(attrs); + _final("$r"); +} + +void vt_stream::decdmac(const vt_parm id, const vt_parm dt, const vt_parm encoding, const std::string_view data) +{ + _string("\033P"); + _parms({id, dt, encoding}); + _string("!z"); + _string(data); + _string("\033\\"); +} + +void vt_stream::decinvm(const vt_parm id) +{ + _csi(); + _parm(id); + _final("*z"); +} + +void vt_stream::decswt(const std::string_view s) +{ + _string("\033]21;"); + write(s); + _string("\033\\"); +} + +void vt_stream::dcs(const std::string_view s) +{ + _string("\033P"); + _string(s); + _string("\033\\"); +} + +void vt_stream::write(const std::string_view s) +{ + _string(s); +} + +void vt_stream::write(const char ch) +{ + _char(ch); +} + +void vt_stream::write(const std::wstring_view s) +{ + iso2022{s}.write(*this); +} + +void vt_stream::write(const wchar_t ch) +{ + iso2022{ch}.write(*this); +} + +void vt_stream::write_spaces(const int count) +{ + for (auto i = 0; i < count; i++) + _char(' '); +} + +void vt_stream::flush() +{ + if (_buffer_index) { + _stream.write(&_buffer[0], _buffer_index); + _stream.flush(); + _buffer_index = 0; + } +} + +void vt_stream::_csi() +{ + _string("\033["); +} + +void vt_stream::_final(const std::string_view chars) +{ + _string(chars); +} + +void vt_stream::_parm(const vt_parm value) +{ + if (value) _number(value); +} + +void vt_stream::_parms(const vt_parms parms, const bool compact) +{ + auto last_good = -1; + auto i = 0; + for (const auto parm : parms) { + if (parm) last_good = i; + i++; + } + i = 0; + for (const auto parm : parms) { + if (i > last_good && compact) break; + if (i > 0) _char(';'); + if (parm) _number(parm); + i++; + } +} + +void vt_stream::_number(const int n) +{ + if (n >= 10) _number(n / 10); + _char('0' + (n % 10)); +} + +void vt_stream::_string(const std::string_view s) +{ + for (auto ch : s) + _char(ch); +} + +void vt_stream::_char(const char ch) +{ + _buffer[_buffer_index++] = ch; + if (_buffer_index == _buffer.size()) + flush(); +} diff --git a/src/vt.h b/src/vt.h new file mode 100644 index 0000000..ffe8b62 --- /dev/null +++ b/src/vt.h @@ -0,0 +1,83 @@ +// VT Font Maker +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include +#include +#include +#include + +using vt_parm = int; +using vt_parms = std::initializer_list; + +class vt_stream { +public: + vt_stream(std::ostream& stream); + ~vt_stream(); + void ls0(); + void ls1(); + void ls2(); + void ls3(); + void cup(const vt_parm row = {}, const vt_parm col = {}); + void cuf(const vt_parm cols = {}); + void cub(const vt_parm cols = {}); + void ed(const vt_parm type = {}); + void il(const vt_parm count = {}); + void sgr(const vt_parms attrs = {}); + void ppa(const vt_parm page = {}); + void sm(const vt_parm mode); + void rm(const vt_parm mode); + void sm(const char prefix, const vt_parm mode); + void rm(const char prefix, const vt_parm mode); + void sm(const vt_parms modes); + void rm(const vt_parms modes); + void sm(const char prefix, const vt_parms modes); + void rm(const char prefix, const vt_parms modes); + void dsr(const vt_parm id); + void dsr(const char prefix, const vt_parm id); + void scs(const int gset, const std::string_view id); + void scs96(const int gset, const std::string_view id); + void da(); + void s7c1t(); + void decsc(); + void decrc(); + void decfi(); + void decic(const vt_parm count = {}); + void decsace(const vt_parm extent = {}); + void decrqm(const char prefix, const vt_parm mode); + void decac(const vt_parm a, const vt_parm b, const vt_parm c); + void decctr(const vt_parm type); + void decstbm(const vt_parm top = {}, const vt_parm bottom = {}); + void decslrm(const vt_parm left = {}, const vt_parm right = {}); + void decfra(const vt_parm ch, const vt_parm top, const vt_parm left, const vt_parm bottom, const vt_parm right); + void deccra(const vt_parm top, const vt_parm left, const vt_parm bottom, const vt_parm right, const vt_parm dtop, const vt_parm dleft); + void deccra(const vt_parm top, const vt_parm left, const vt_parm bottom, const vt_parm right, const vt_parm page, const vt_parm dtop, const vt_parm dleft, const vt_parm dpage); + void deccara(const vt_parm top, const vt_parm left, const vt_parm bottom, const vt_parm right, const vt_parms attrs); + void decdmac(const vt_parm id, const vt_parm dt, const vt_parm encoding, const std::string_view data = ""); + void decinvm(const vt_parm id); + void decswt(const std::string_view s = {}); + void dcs(const std::string_view s); + void write(const std::string_view s); + void write(const char ch); + void write(const std::wstring_view s); + void write(const wchar_t ch); + void write_spaces(const int count); + void flush(); + +private: + void _csi(); + void _final(const std::string_view chars); + void _parm(const vt_parm value); + void _parms(const vt_parms parms, const bool compact = true); + void _number(const int n); + void _string(const std::string_view s); + void _char(const char ch); + + std::ostream& _stream; + std::array _buffer = {}; + int _buffer_index = 0; +}; + +extern vt_stream vtout;