summaryrefslogtreecommitdiffhomepage
path: root/editor
diff options
context:
space:
mode:
Diffstat (limited to 'editor')
-rw-r--r--editor/CMakeLists.txt14
-rw-r--r--editor/app.cpp0
-rw-r--r--editor/app.hpp0
-rw-r--r--editor/camera.cpp88
-rw-r--r--editor/editor.cpp234
-rw-r--r--editor/editor.hpp93
-rw-r--r--editor/imgui-raii.hpp86
-rw-r--r--editor/imgui.cpp240
-rw-r--r--editor/keyboard.cpp38
-rw-r--r--editor/update.cpp55
10 files changed, 848 insertions, 0 deletions
diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt
new file mode 100644
index 00000000..8d3a0bf5
--- /dev/null
+++ b/editor/CMakeLists.txt
@@ -0,0 +1,14 @@
+set(self ${PROJECT_NAME}-editor)
+
+corrade_add_resource(res ../resources.conf)
+
+if(MSVC)
+ set_property(SOURCE "${res}" APPEND PROPERTY COMPILE_OPTIONS "-W0")
+else()
+ set_property(SOURCE "${res}" APPEND PROPERTY COMPILE_OPTIONS "-w")
+endif()
+
+add_executable(${self} "${res}" "../loader/loader-impl.cpp")
+target_link_libraries(${self} ${PROJECT_NAME}-main)
+
+install(TARGETS ${self} RUNTIME DESTINATION bin)
diff --git a/editor/app.cpp b/editor/app.cpp
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/editor/app.cpp
diff --git a/editor/app.hpp b/editor/app.hpp
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/editor/app.hpp
diff --git a/editor/camera.cpp b/editor/camera.cpp
new file mode 100644
index 00000000..7af77211
--- /dev/null
+++ b/editor/camera.cpp
@@ -0,0 +1,88 @@
+#include "app.hpp"
+#include <Magnum/GL/DefaultFramebuffer.h>
+
+namespace floormat {
+
+void app::do_camera(double dt)
+{
+ if (keys[key::camera_reset])
+ reset_camera_offset();
+ else
+ {
+ Vector2d dir{};
+
+ if (keys[key::camera_up])
+ dir += Vector2d{0, -1};
+ else if (keys[key::camera_down])
+ dir += Vector2d{0, 1};
+ if (keys[key::camera_left])
+ dir += Vector2d{-1, 0};
+ else if (keys[key::camera_right])
+ dir += Vector2d{1, 0};
+
+ if (dir != Vector2d{})
+ {
+ constexpr double screens_per_second = 1;
+ const auto pixels_per_second = windowSize().length() / screens_per_second;
+ auto camera_offset = _shader.camera_offset();
+ const auto max_camera_offset = Vector2d(windowSize() * 10);
+
+ camera_offset -= dir.normalized() * dt * pixels_per_second;
+ camera_offset[0] = std::clamp(camera_offset[0], -max_camera_offset[0], max_camera_offset[0]);
+ camera_offset[1] = std::clamp(camera_offset[1], -max_camera_offset[1], max_camera_offset[1]);
+
+ _shader.set_camera_offset(camera_offset);
+ }
+ else
+ return;
+ }
+ recalc_cursor_tile();
+ if (_cursor_tile)
+ do_mouse_move(*_cursor_tile);
+}
+
+void app::reset_camera_offset()
+{
+ _shader.set_camera_offset(tile_shader::project({TILE_MAX_DIM*-.5*dTILE_SIZE[0], TILE_MAX_DIM*-.5*dTILE_SIZE[1], 0}));
+ recalc_cursor_tile();
+}
+
+void app::recalc_cursor_tile()
+{
+ if (_cursor_pixel && !_cursor_in_imgui)
+ _cursor_tile = pixel_to_tile(Vector2d(*_cursor_pixel));
+ else
+ _cursor_tile = std::nullopt;
+}
+
+global_coords app::pixel_to_tile(Vector2d position) const
+{
+ constexpr Vector2d pixel_size{dTILE_SIZE[0], dTILE_SIZE[1]};
+ constexpr Vector2d half{.5, .5};
+ const Vector2d px = position - Vector2d{windowSize()}*.5 - _shader.camera_offset()*.5;
+ const Vector2d vec = tile_shader::unproject(px) / pixel_size + half;
+ const auto x = (std::int32_t)std::floor(vec[0]), y = (std::int32_t)std::floor(vec[1]);
+ return { x, y };
+}
+
+std::array<std::int16_t, 4> app::get_draw_bounds() const noexcept
+{
+
+ using limits = std::numeric_limits<std::int16_t>;
+ auto x0 = limits::max(), x1 = limits::min(), y0 = limits::max(), y1 = limits::min();
+
+ for (const auto win = Vector2d(windowSize());
+ auto p : {pixel_to_tile(Vector2d{0, 0}).chunk(),
+ pixel_to_tile(Vector2d{win[0]-1, 0}).chunk(),
+ pixel_to_tile(Vector2d{0, win[1]-1}).chunk(),
+ pixel_to_tile(Vector2d{win[0]-1, win[1]-1}).chunk()})
+ {
+ x0 = std::min(x0, p.x);
+ x1 = std::max(x1, p.x);
+ y0 = std::min(y0, p.y);
+ y1 = std::max(y1, p.y);
+ }
+ return {x0, x1, y0, y1};
+}
+
+} // namespace floormat
diff --git a/editor/editor.cpp b/editor/editor.cpp
new file mode 100644
index 00000000..f3c8b157
--- /dev/null
+++ b/editor/editor.cpp
@@ -0,0 +1,234 @@
+#include "editor.hpp"
+#include "serialize/json-helper.hpp"
+#include "serialize/tile-atlas.hpp"
+#include "src/loader.hpp"
+#include "random.hpp"
+#include "compat/assert.hpp"
+#include "compat/unreachable.hpp"
+#include "src/tile-defs.hpp"
+#include "src/world.hpp"
+#include <Corrade/Containers/StringStlView.h>
+#include <filesystem>
+#include <vector>
+
+namespace floormat {
+
+static const std::filesystem::path image_path{IMAGE_PATH, std::filesystem::path::generic_format};
+
+tile_type::tile_type(editor_mode mode, Containers::StringView name) : _name{name}, _mode{mode}
+{
+ load_atlases();
+}
+
+void tile_type::load_atlases()
+{
+ using atlas_array = std::vector<std::shared_ptr<tile_atlas>>;
+ for (auto& atlas : json_helper::from_json<atlas_array>(image_path/(_name + ".json")))
+ {
+ Containers::StringView name = atlas->name();
+ if (auto x = name.findLast('.'); x)
+ name = name.prefix(x.data());
+ auto& [_, vec] = _permutation;
+ vec.reserve((std::size_t)atlas->num_tiles());
+ _atlases[name] = std::move(atlas);
+ }
+}
+
+std::shared_ptr<tile_atlas> tile_type::maybe_atlas(Containers::StringView str)
+{
+ auto it = std::find_if(_atlases.begin(), _atlases.end(), [&](const auto& tuple) -> bool {
+ const auto& [x, _] = tuple;
+ return Containers::StringView{x} == str;
+ });
+ if (it == _atlases.end())
+ return nullptr;
+ else
+ return it->second;
+}
+
+std::shared_ptr<tile_atlas> tile_type::atlas(Containers::StringView str)
+{
+ if (auto ptr = maybe_atlas(str); ptr)
+ return ptr;
+ else
+ fm_abort("no such atlas: %s", str.cbegin());
+}
+
+void tile_type::clear_selection()
+{
+ _selected_tile = {};
+ _permutation = {};
+ _selection_mode = sel_none;
+}
+
+void tile_type::select_tile(const std::shared_ptr<tile_atlas>& atlas, std::size_t variant)
+{
+ fm_assert(atlas);
+ clear_selection();
+ _selection_mode = sel_tile;
+ _selected_tile = { atlas, variant % atlas->num_tiles() };
+}
+
+void tile_type::select_tile_permutation(const std::shared_ptr<tile_atlas>& atlas)
+{
+ fm_assert(atlas);
+ clear_selection();
+ _selection_mode = sel_perm;
+ _permutation = { atlas, {} };
+}
+
+bool tile_type::is_tile_selected(const std::shared_ptr<const tile_atlas>& atlas, std::size_t variant) const
+{
+ return atlas && _selection_mode == sel_tile && _selected_tile &&
+ atlas == _selected_tile.atlas && variant == _selected_tile.variant;
+}
+
+bool tile_type::is_permutation_selected(const std::shared_ptr<const tile_atlas>& atlas) const
+{
+ const auto& [perm, _] = _permutation;
+ return atlas && _selection_mode == sel_perm && perm == atlas;
+}
+
+bool tile_type::is_atlas_selected(const std::shared_ptr<const tile_atlas>& atlas) const
+{
+ switch (_selection_mode)
+ {
+ default:
+ case sel_none:
+ return false;
+ case sel_perm:
+ return is_permutation_selected(atlas);
+ case sel_tile:
+ return atlas && _selected_tile && atlas == _selected_tile.atlas;
+ }
+}
+
+template<std::random_access_iterator T>
+void fisher_yates(T begin, T end)
+{
+ const auto N = std::distance(begin, end);
+ for (auto i = N-1; i >= 1; i--)
+ {
+ const auto j = random(i+1);
+ using std::swap;
+ swap(begin[i], begin[j]);
+ }
+}
+
+tile_image tile_type::get_selected_perm()
+{
+ auto& [atlas, vec] = _permutation;
+ const std::size_t N = atlas->num_tiles();
+ if (N == 0)
+ return {};
+ if (vec.empty())
+ {
+ for (std::size_t i = 0; i < N; i++)
+ vec.push_back(i);
+ fisher_yates(vec.begin(), vec.end());
+ }
+ const auto idx = vec.back();
+ vec.pop_back();
+ return {atlas, idx};
+}
+
+tile_image tile_type::get_selected()
+{
+ switch (_selection_mode)
+ {
+ case sel_none:
+ return {};
+ case sel_tile:
+ return _selected_tile;
+ case sel_perm:
+ return get_selected_perm();
+ default:
+ fm_warn_once("invalid editor mode '%u'", (unsigned)_selection_mode);
+ break;
+ }
+}
+
+void tile_type::place_tile(world& world, global_coords pos, tile_image& img)
+{
+ const auto& [c, t] = world[pos];
+ const auto& [atlas, variant] = img;
+ switch (_mode)
+ {
+ default:
+ fm_warn_once("invalid editor mode '%u'", (unsigned)_mode);
+ break;
+ case editor_mode::select:
+ break;
+ case editor_mode::floor: {
+ const auto& [c, t] = world[pos];
+ t.ground_image = { atlas, variant };
+ break;
+ }
+ case editor_mode::walls: {
+ break; // todo
+ }
+ }
+}
+
+editor::editor()
+{
+ set_mode(editor_mode::floor); // TODO
+}
+
+void editor::set_mode(editor_mode mode)
+{
+ _mode = mode;
+ on_release();
+}
+
+const tile_type* editor::current() const
+{
+ switch (_mode)
+ {
+ case editor_mode::select:
+ return nullptr;
+ case editor_mode::floor:
+ return &_floor;
+ case editor_mode::walls:
+ return nullptr; // todo
+ default:
+ fm_warn_once("invalid editor mode '%u'", (unsigned)_mode);
+ return nullptr;
+ }
+}
+
+tile_type* editor::current()
+{
+ return const_cast<tile_type*>(static_cast<const editor&>(*this).current());
+}
+
+void editor::on_release()
+{
+ _last_pos = std::nullopt;
+}
+
+void editor::on_mouse_move(world& world, const global_coords pos)
+{
+ if (_last_pos && *_last_pos != pos)
+ {
+ _last_pos = pos;
+ on_click(world, pos);
+ }
+}
+
+void editor::on_click(world& world, global_coords pos)
+{
+ if (auto* mode = current(); mode)
+ {
+ auto opt = mode->get_selected();
+ if (opt)
+ {
+ _last_pos = pos;
+ mode->place_tile(world, pos, opt);
+ }
+ else
+ on_release();
+ }
+}
+
+} // namespace floormat
diff --git a/editor/editor.hpp b/editor/editor.hpp
new file mode 100644
index 00000000..28ba153c
--- /dev/null
+++ b/editor/editor.hpp
@@ -0,0 +1,93 @@
+#pragma once
+#include "compat/defs.hpp"
+#include "tile-atlas.hpp"
+#include "global-coords.hpp"
+#include "tile.hpp"
+
+#include <cstdint>
+#include <tuple>
+#include <optional>
+#include <vector>
+#include <map>
+#include <memory>
+#include <Corrade/Containers/StringView.h>
+
+namespace floormat {
+
+enum class editor_mode : unsigned char {
+ select, floor, walls,
+};
+
+struct world;
+
+struct tile_type final
+{
+ tile_type(editor_mode mode, Containers::StringView name);
+ std::shared_ptr<tile_atlas> maybe_atlas(Containers::StringView str);
+ std::shared_ptr<tile_atlas> atlas(Containers::StringView str);
+ auto cbegin() const { return _atlases.cbegin(); }
+ auto cend() const { return _atlases.cend(); }
+ auto begin() const { return _atlases.cbegin(); }
+ auto end() const { return _atlases.cend(); }
+ Containers::StringView name() const { return _name; }
+ editor_mode mode() const { return _mode; }
+
+ void clear_selection();
+ void select_tile(const std::shared_ptr<tile_atlas>& atlas, std::size_t variant);
+ void select_tile_permutation(const std::shared_ptr<tile_atlas>& atlas);
+ bool is_tile_selected(const std::shared_ptr<const tile_atlas>& atlas, std::size_t variant) const;
+ bool is_permutation_selected(const std::shared_ptr<const tile_atlas>& atlas) const;
+ bool is_atlas_selected(const std::shared_ptr<const tile_atlas>& atlas) const;
+ tile_image get_selected();
+ void place_tile(world& world, global_coords pos, tile_image& img);
+
+private:
+ enum selection_mode : std::uint8_t {
+ sel_none, sel_tile, sel_perm,
+ };
+ enum rotation : std::uint8_t {
+ rot_N, rot_W,
+ };
+
+ std::string _name;
+ std::map<std::string, std::shared_ptr<tile_atlas>> _atlases;
+ tile_image _selected_tile;
+ std::tuple<std::shared_ptr<tile_atlas>, std::vector<std::size_t>> _permutation;
+ selection_mode _selection_mode = sel_none;
+ editor_mode _mode;
+ rotation _rotation{};
+
+ void load_atlases();
+ tile_image get_selected_perm();
+};
+
+struct editor final
+{
+ [[nodiscard]] bool dirty() const { return _dirty; }
+ void set_dirty(bool value) { _dirty = value; }
+ [[nodiscard]] editor_mode mode() const { return _mode; }
+ void set_mode(editor_mode mode);
+
+ tile_type& floor() { return _floor; }
+ const tile_type& floor() const { return _floor; }
+
+ tile_type* current();
+ const tile_type* current() const;
+
+ void on_click(world& world, global_coords pos);
+ void on_mouse_move(world& world, const global_coords pos);
+ void on_release();
+
+ editor();
+ editor(editor&&) noexcept = default;
+ editor& operator=(editor&&) noexcept = default;
+ fm_DECLARE_DELETED_COPY_ASSIGNMENT(editor);
+
+private:
+ tile_type _floor{editor_mode::floor, "floor"};
+ std::optional<global_coords> _last_pos;
+ editor_mode _mode = editor_mode::select;
+ bool _dirty = false;
+};
+
+} // namespace floormat
diff --git a/editor/imgui-raii.hpp b/editor/imgui-raii.hpp
new file mode 100644
index 00000000..afae29d6
--- /dev/null
+++ b/editor/imgui-raii.hpp
@@ -0,0 +1,86 @@
+#pragma once
+
+#include <Corrade/Containers/StringView.h>
+#include <Magnum/Math/Color.h>
+#ifndef __CLION_IDE__zz
+#include <imgui.h>
+#endif
+
+namespace floormat::imgui {
+
+struct raii_wrapper final
+{
+ using F = void(*)(void);
+ raii_wrapper(F fn) : dtor{fn} {}
+ raii_wrapper() = default;
+ ~raii_wrapper() { if (dtor) dtor(); }
+ raii_wrapper(const raii_wrapper&) = delete;
+ raii_wrapper& operator=(const raii_wrapper&) = delete;
+ raii_wrapper& operator=(raii_wrapper&&) = delete;
+ raii_wrapper(raii_wrapper&& other) noexcept : dtor{other.dtor} { other.dtor = nullptr; }
+ inline operator bool() const noexcept { return dtor != nullptr; }
+
+ F dtor = nullptr;
+};
+
+[[nodiscard]] static inline raii_wrapper begin_window(Containers::StringView name = {},
+ ImGuiWindowFlags_ flags = ImGuiWindowFlags_None)
+{
+ if (name.isEmpty())
+ name = "floormat editor";
+ if (ImGui::Begin(name.data(), nullptr, flags))
+ return {&ImGui::End};
+ else
+ return {};
+}
+
+[[nodiscard]] static inline raii_wrapper begin_main_menu()
+{
+ if (ImGui::BeginMainMenuBar())
+ return {&ImGui::EndMainMenuBar};
+ else
+ return {};
+}
+[[nodiscard]] static inline raii_wrapper begin_menu(Containers::StringView name, bool enabled = true)
+{
+ if (ImGui::BeginMenu(name.data(), enabled))
+ return {&ImGui::EndMenu};
+ else
+ return {};
+}
+
+[[nodiscard]] static inline raii_wrapper begin_list_box(Containers::StringView name, ImVec2 size = {})
+{
+ if (ImGui::BeginListBox(name.data(), size))
+ return {&ImGui::EndListBox};
+ else
+ return {};
+}
+
+[[nodiscard]] static inline raii_wrapper tree_node(Containers::StringView name, ImGuiTreeNodeFlags_ flags = ImGuiTreeNodeFlags_None)
+{
+ if (ImGui::TreeNodeEx(name.data(), flags))
+ return {&ImGui::TreePop};
+ else
+ return {};
+}
+
+[[nodiscard]] static inline raii_wrapper push_style_var(ImGuiStyleVar_ var, Vector2 value)
+{
+ ImGui::PushStyleVar(var, {value[0], value[1]});
+ return {[]{ ImGui::PopStyleVar(); }};
+}
+
+[[nodiscard]] static inline raii_wrapper push_style_var(ImGuiStyleVar_ var, float value)
+{
+ ImGui::PushStyleVar(var, value);
+ return {[]{ ImGui::PopStyleVar(); }};
+}
+
+[[nodiscard]] static inline raii_wrapper push_style_color(ImGuiCol_ var, const Color4& value)
+{
+ ImGui::PushStyleColor(var, {value[0], value[1], value[2], value[3]});
+ return {[]{ ImGui::PopStyleColor(); }};
+}
+
+} // namespace floormat::imgui
diff --git a/editor/imgui.cpp b/editor/imgui.cpp
new file mode 100644
index 00000000..b0777d5d
--- /dev/null
+++ b/editor/imgui.cpp
@@ -0,0 +1,240 @@
+#include "app.hpp"
+#include <Magnum/GL/Renderer.h>
+#include "imgui-raii.hpp"
+#include <Magnum/ImGuiIntegration/Context.h>
+
+namespace floormat {
+
+using namespace floormat::imgui;
+
+void app::init_imgui(Vector2i size)
+{
+ if (!_imgui.context())
+ _imgui = ImGuiIntegration::Context(Vector2{size}, size, size);
+ else
+ _imgui.relayout(Vector2{size}, size, size);
+}
+
+void app::render_menu()
+{
+ GL::Renderer::setBlendEquation(GL::Renderer::BlendEquation::Add, GL::Renderer::BlendEquation::Add);
+ GL::Renderer::setBlendFunction(GL::Renderer::BlendFunction::SourceAlpha, GL::Renderer::BlendFunction::OneMinusSourceAlpha);
+ GL::Renderer::enable(GL::Renderer::Feature::Blending);
+
+ GL::Renderer::enable(GL::Renderer::Feature::ScissorTest);
+ GL::Renderer::disable(GL::Renderer::Feature::FaceCulling);
+ GL::Renderer::disable(GL::Renderer::Feature::DepthTest);
+
+ _imgui.drawFrame();
+}
+
+float app::draw_main_menu()
+{
+ float main_menu_height = 0;
+ if (auto b = begin_main_menu())
+ {
+ if (auto b = begin_menu("File"))
+ {
+ ImGui::MenuItem("Open", "Ctrl+O");
+ ImGui::MenuItem("Recent");
+ ImGui::Separator();
+ ImGui::MenuItem("Save", "Ctrl+S");
+ ImGui::MenuItem("Save as...", "Ctrl+Shift+S");
+ ImGui::Separator();
+ ImGui::MenuItem("Close");
+ }
+ if (auto b = begin_menu("Mode"))
+ {
+ ImGui::MenuItem("Select", "F1", _editor.mode() == editor_mode::select);
+ ImGui::MenuItem("Floor", "F2", _editor.mode() == editor_mode::floor);
+ ImGui::MenuItem("Walls", "F3", _editor.mode() == editor_mode::walls);
+ }
+
+ main_menu_height = ImGui::GetContentRegionMax().y;
+ }
+ return main_menu_height;
+}
+
+void app::draw_ui()
+{
+ ImGui::GetIO().IniFilename = nullptr;
+ _imgui.newFrame();
+ ImGui::StyleColorsDark(&ImGui::GetStyle());
+
+ const float main_menu_height = draw_main_menu();
+ draw_editor_pane(_editor.floor(), main_menu_height);
+ draw_fps();
+ draw_cursor_coord();
+ ImGui::EndFrame();
+}
+
+void app::draw_editor_pane(tile_type& type, float main_menu_height)
+{
+ constexpr
+ Color4 color_perm_selected{1, 1, 1, .7f},
+ color_selected{1, 0.843f, 0, .8f},
+ color_hover{0, .8f, 1, .7f};
+
+ if (ImGui::GetIO().WantTextInput && !isTextInputActive())
+ startTextInput();
+ else if (!ImGui::GetIO().WantTextInput && isTextInputActive())
+ stopTextInput();
+
+ [[maybe_unused]] const raii_wrapper vars[] = {
+ push_style_var(ImGuiStyleVar_WindowPadding, {8, 8}),
+ push_style_var(ImGuiStyleVar_WindowBorderSize, 0),
+ push_style_var(ImGuiStyleVar_FramePadding, {4, 4}),
+ push_style_color(ImGuiCol_WindowBg, {0, 0, 0, .5}),
+ push_style_color(ImGuiCol_FrameBg, {0, 0, 0, 0}),
+ };
+
+ const auto& style = ImGui::GetStyle();
+ tile_type* const ed = _editor.current();
+
+ if (main_menu_height > 0)
+ {
+ ImGui::SetNextWindowPos({0, main_menu_height+style.WindowPadding.y});
+ ImGui::SetNextFrameWantCaptureKeyboard(false);
+ ImGui::SetNextWindowSize({420, windowSize()[1] - main_menu_height - style.WindowPadding.y});
+ if (const auto flags = ImGuiWindowFlags_(ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings);
+ auto b = begin_window({}, flags))
+ {
+ const float window_width = ImGui::GetWindowWidth() - 32;
+
+ char buf[128];
+ //ImGui::SetNextWindowBgAlpha(.2f);
+
+ if (auto b = begin_list_box("##atlases", {-FLT_MIN, -1}))
+ {
+ for (const auto& [k, v] : type)
+ {
+ ///const auto& k_ = k;
+ const auto& v_ = v;
+ const auto click_event = [&] {
+ if (ed)
+ {
+ if (ImGui::IsItemClicked(ImGuiMouseButton_Right))
+ ed->select_tile_permutation(v_);
+ else if (ImGui::IsItemClicked(ImGuiMouseButton_Middle))
+ ed->clear_selection();
+ }
+ };
+ const auto do_caption = [&] {
+ if (ed)
+ {
+ click_event();
+ if (ed->is_atlas_selected(v))
+ {
+ ImGui::SameLine();
+ ImGui::Text(" (selected)");
+ }
+ }
+ {
+ snprintf(buf, sizeof(buf), "%zu", (std::size_t)v_->num_tiles());
+ ImGui::SameLine(window_width - ImGui::CalcTextSize(buf).x - style.FramePadding.x - 4);
+ ImGui::Text("%s", buf);
+ }
+ };
+ const auto N = v->num_tiles();
+ if (const auto flags = ImGuiTreeNodeFlags_(ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_Framed);
+ auto b = tree_node(k.data(), flags))
+ {
+ do_caption();
+ [[maybe_unused]] const raii_wrapper vars[] = {
+ push_style_var(ImGuiStyleVar_FramePadding, {2, 2}),
+ push_style_color(ImGuiCol_ButtonHovered, color_hover),
+ };
+ const bool perm_selected = ed ? ed->is_permutation_selected(v) : false;
+ constexpr std::size_t per_row = 8;
+ for (std::size_t i = 0; i < N; i++)
+ {
+ const bool selected = ed ? ed->is_tile_selected(v, i) : false;
+
+ if (i > 0 && i % per_row == 0)
+ ImGui::NewLine();
+
+ [[maybe_unused]] const raii_wrapper vars[] = {
+ selected ? push_style_color(ImGuiCol_Button, color_selected) : raii_wrapper{},
+ selected ? push_style_color(ImGuiCol_ButtonHovered, color_selected) : raii_wrapper{},
+ perm_selected ? push_style_color(ImGuiCol_Button, color_perm_selected) : raii_wrapper{},
+ perm_selected ? push_style_color(ImGuiCol_ButtonHovered, color_perm_selected) : raii_wrapper{},
+ };
+
+ snprintf(buf, sizeof(buf), "##item_%zu", i);
+ const auto uv = v->texcoords_for_id(i);
+ ImGui::ImageButton(buf, (void*)&v->texture(), {TILE_SIZE[0]/2, TILE_SIZE[1]/2},
+ { uv[3][0], uv[3][1] }, { uv[0][0], uv[0][1] });
+ if (ed)
+ {
+ if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
+ ed->select_tile(v, i);
+ else
+ click_event();
+ }
+ ImGui::SameLine();
+ }
+ ImGui::NewLine();
+ }
+ else
+ do_caption();
+ }
+ }
+ }
+ }
+}
+
+void app::draw_fps()
+{
+ auto c1 = push_style_var(ImGuiStyleVar_FramePadding, {0, 0});
+ auto c2 = push_style_var(ImGuiStyleVar_WindowPadding, {0, 0});
+ auto c3 = push_style_var(ImGuiStyleVar_WindowBorderSize, 0);
+ auto c4 = push_style_var(ImGuiStyleVar_WindowMinSize, {1, 1});
+ auto c5 = push_style_var(ImGuiStyleVar_ScrollbarSize, 0);
+ auto c6 = push_style_color(ImGuiCol_Text, {0, 1, 0, 1});
+
+ char buf[16];
+ const double hz = _frame_time > 1e-6f ? (int)std::round(10./(double)_frame_time + .05) * .1 : 9999;
+ snprintf(buf, sizeof(buf), "%.1f FPS", hz);
+ const ImVec2 size = ImGui::CalcTextSize(buf);
+
+ ImGui::SetNextWindowPos({windowSize()[0] - size.x - 4, 3});
+ ImGui::SetNextWindowSize(size);
+
+ if (auto flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoInputs |
+ ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBackground;
+ auto b = begin_window("framerate", ImGuiWindowFlags_(flags)))
+ {
+ ImGui::Text("%s", buf);
+ }
+}
+
+void app::draw_cursor_coord()
+{
+ if (!_cursor_tile)
+ return;
+
+ auto c1 = push_style_var(ImGuiStyleVar_FramePadding, {0, 0});
+ auto c2 = push_style_var(ImGuiStyleVar_WindowPadding, {0, 0});
+ auto c3 = push_style_var(ImGuiStyleVar_WindowBorderSize, 0);
+ auto c4 = push_style_var(ImGuiStyleVar_WindowMinSize, {1, 1});
+ auto c5 = push_style_var(ImGuiStyleVar_ScrollbarSize, 0);
+ auto c6 = push_style_color(ImGuiCol_Text, {.9f, .9f, .9f, 1});
+
+ char buf[64];
+ const auto coord = *_cursor_tile;
+ const auto chunk = coord.chunk();
+ const auto local = coord.local();
+ snprintf(buf, sizeof(buf), "%hd:%hd - %hhu:%hhu", chunk.x, chunk.y, local.x, local.y);
+ const auto size = ImGui::CalcTextSize(buf);
+
+ ImGui::SetNextWindowPos({windowSize()[0]/2 - size.x/2, 3});
+ ImGui::SetNextWindowSize(size);
+ if (auto flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoInputs |
+ ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBackground;
+ auto b = begin_window("tile-coord", ImGuiWindowFlags_(flags)))
+ {
+ ImGui::Text("%s", buf);
+ }
+}
+
+} // namespace floormat
diff --git a/editor/keyboard.cpp b/editor/keyboard.cpp
new file mode 100644
index 00000000..a700d0f4
--- /dev/null
+++ b/editor/keyboard.cpp
@@ -0,0 +1,38 @@
+#include "app.hpp"
+
+namespace floormat {
+
+void app::do_key(KeyEvent::Key k, KeyEvent::Modifiers m, bool pressed, bool repeated)
+{
+ //using Mods = KeyEvent::Modifiers;
+
+ (void)m;
+ (void)repeated;
+
+ const key x = fm_begin(switch (k)
+ {
+ using enum KeyEvent::Key;
+ using enum key;
+
+ default: return COUNT;
+ case W: return camera_up;
+ case A: return camera_left;
+ case S: return camera_down;
+ case D: return camera_right;
+ case Home: return camera_reset;
+ case R: return rotate_tile;
+ case F5: return quicksave;
+ case F9: return quickload;
+ case Esc: return quit;
+ });
+
+ if (x != key::COUNT)
+ keys[x] = pressed;
+}
+
+app::~app()
+{
+ loader_::destroy();
+}
+
+} // namespace floormat
diff --git a/editor/update.cpp b/editor/update.cpp
new file mode 100644
index 00000000..ebd1881b
--- /dev/null
+++ b/editor/update.cpp
@@ -0,0 +1,55 @@
+#include "app.hpp"
+
+namespace floormat {
+
+//#define FM_NO_BINDINGS
+
+void app::make_test_chunk(chunk& c)
+{
+ constexpr auto N = TILE_MAX_DIM;
+ for (auto [x, k, pt] : c) {
+#if defined FM_NO_BINDINGS
+ const auto& atlas = floor1;
+#else
+ const auto& atlas = pt.x != pt.y && (pt.x == N/2 || pt.y == N/2) ? floor2 : floor1;
+#endif
+ x.ground_image = { atlas, k % atlas->num_tiles() };
+ }
+#ifdef FM_NO_BINDINGS
+ const auto& wall1 = floor1, wall2 = floor1;
+#endif
+ constexpr auto K = N/2;
+ c[{K, K }].wall_north = { wall1, 0 };
+ c[{K, K }].wall_west = { wall2, 0 };
+ c[{K, K+1}].wall_north = { wall1, 0 };
+ c[{K+1, K }].wall_west = { wall2, 0 };
+}
+
+void app::do_mouse_click(const global_coords pos, int button)
+{
+ if (button == SDL_BUTTON_LEFT)
+ _editor.on_click(_world, pos);
+ else
+ _editor.on_release();
+}
+
+void app::do_mouse_release(int button)
+{
+ (void)button;
+ _editor.on_release();
+}
+
+void app::do_mouse_move(global_coords pos)
+{
+ _editor.on_mouse_move(_world, pos);
+}
+
+void app::update(double dt)
+{
+ do_camera(dt);
+ draw_ui();
+ if (keys[key::quit])
+ Platform::Sdl2Application::exit(0);
+}
+
+} // namespace floormat