|
|
@@ -0,0 +1,345 @@
|
|
|
+#include "lumacs/raylib_editor.hpp"
|
|
|
+#include "lumacs/editor_core.hpp"
|
|
|
+#include "lumacs/logger.hpp"
|
|
|
+#include "lumacs/lua_api.hpp"
|
|
|
+#include "lumacs/keybinding.hpp"
|
|
|
+#include "lumacs/command_system.hpp"
|
|
|
+#include "lumacs/minibuffer_manager.hpp"
|
|
|
+#include "lumacs/window_manager.hpp"
|
|
|
+#include "lumacs/layout_node.hpp"
|
|
|
+#include "lumacs/theme.hpp"
|
|
|
+#include "lumacs/buffer.hpp"
|
|
|
+#include "lumacs/buffer_manager.hpp"
|
|
|
+#include <raylib.h>
|
|
|
+#include <spdlog/spdlog.h>
|
|
|
+#include <string>
|
|
|
+#include <vector>
|
|
|
+#include <fmt/core.h>
|
|
|
+
|
|
|
+namespace lumacs {
|
|
|
+
|
|
|
+// Helper to construct Raylib colors explicitly to avoid namespace collision with lumacs::Color
|
|
|
+static ::Color RL_Color(unsigned char r, unsigned char g, unsigned char b, unsigned char a = 255) {
|
|
|
+ return ::Color{r, g, b, a};
|
|
|
+}
|
|
|
+
|
|
|
+static ::Color ToRLColor(const lumacs::Color& c) {
|
|
|
+ if (c.r == -1) return RL_Color(0, 0, 0, 0);
|
|
|
+ return RL_Color((unsigned char)c.r, (unsigned char)c.g, (unsigned char)c.b);
|
|
|
+}
|
|
|
+
|
|
|
+RaylibEditor::RaylibEditor() {
|
|
|
+}
|
|
|
+
|
|
|
+RaylibEditor::~RaylibEditor() {
|
|
|
+ if (IsWindowReady()) {
|
|
|
+ CloseWindow();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RaylibEditor::init() {
|
|
|
+ InitWindow(1280, 800, "Lumacs (Raylib)");
|
|
|
+ SetTargetFPS(60);
|
|
|
+ SetExitKey(0); // Disable ESC to exit
|
|
|
+
|
|
|
+ font_ = GetFontDefault();
|
|
|
+ font_size_ = 20;
|
|
|
+ line_height_ = 20;
|
|
|
+ char_width_ = 10;
|
|
|
+
|
|
|
+ const char* font_paths[] = {
|
|
|
+ "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
|
|
+ "/usr/share/fonts/TTF/DejaVuSansMono.ttf",
|
|
|
+ "resources/font.ttf"
|
|
|
+ };
|
|
|
+
|
|
|
+ for (const char* path : font_paths) {
|
|
|
+ if (FileExists(path)) {
|
|
|
+ Font loaded = LoadFontEx(path, font_size_, 0, 0);
|
|
|
+ if (loaded.texture.id != 0) {
|
|
|
+ font_ = loaded;
|
|
|
+ SetTextureFilter(font_.texture, TEXTURE_FILTER_BILINEAR);
|
|
|
+ Vector2 size = MeasureTextEx(font_, "M", (float)font_size_, 1.0f);
|
|
|
+ char_width_ = (int)size.x;
|
|
|
+ line_height_ = (int)size.y;
|
|
|
+ spdlog::info("Loaded font: {}", path);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ spdlog::info("Raylib initialized. Char size: {}x{}", char_width_, line_height_);
|
|
|
+}
|
|
|
+
|
|
|
+void RaylibEditor::set_core(EditorCore* core) {
|
|
|
+ core_ = core;
|
|
|
+}
|
|
|
+
|
|
|
+void RaylibEditor::handle_editor_event(EditorEvent event) {
|
|
|
+ if (event == EditorEvent::Quit) {
|
|
|
+ should_close_ = true;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RaylibEditor::run() {
|
|
|
+ while (!WindowShouldClose() && !should_close_) {
|
|
|
+ update();
|
|
|
+ render();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RaylibEditor::update() {
|
|
|
+ process_input();
|
|
|
+}
|
|
|
+
|
|
|
+static std::string resolve_raylib_key(int key, bool ctrl, bool alt, bool shift) {
|
|
|
+ std::string key_str;
|
|
|
+ switch (key) {
|
|
|
+ case KEY_ENTER: key_str = "Return"; break;
|
|
|
+ case KEY_BACKSPACE: key_str = "Backspace"; break;
|
|
|
+ case KEY_TAB: key_str = "Tab"; break;
|
|
|
+ case KEY_ESCAPE: key_str = "Escape"; break;
|
|
|
+ case KEY_DELETE: key_str = "Delete"; break;
|
|
|
+ case KEY_UP: key_str = "ArrowUp"; break;
|
|
|
+ case KEY_DOWN: key_str = "ArrowDown"; break;
|
|
|
+ case KEY_LEFT: key_str = "ArrowLeft"; break;
|
|
|
+ case KEY_RIGHT: key_str = "ArrowRight"; break;
|
|
|
+ case KEY_HOME: key_str = "Home"; break;
|
|
|
+ case KEY_END: key_str = "End"; break;
|
|
|
+ case KEY_PAGE_UP: key_str = "PageUp"; break;
|
|
|
+ case KEY_PAGE_DOWN: key_str = "PageDown"; break;
|
|
|
+ case KEY_F1: key_str = "F1"; break;
|
|
|
+ case KEY_F2: key_str = "F2"; break;
|
|
|
+ case KEY_F3: key_str = "F3"; break;
|
|
|
+ case KEY_F4: key_str = "F4"; break;
|
|
|
+ case KEY_F5: key_str = "F5"; break;
|
|
|
+ case KEY_F6: key_str = "F6"; break;
|
|
|
+ case KEY_F7: key_str = "F7"; break;
|
|
|
+ case KEY_F8: key_str = "F8"; break;
|
|
|
+ case KEY_F9: key_str = "F9"; break;
|
|
|
+ case KEY_F10: key_str = "F10"; break;
|
|
|
+ case KEY_F11: key_str = "F11"; break;
|
|
|
+ case KEY_F12: key_str = "F12"; break;
|
|
|
+ default: break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (key_str.empty()) {
|
|
|
+ if (key >= KEY_A && key <= KEY_Z) {
|
|
|
+ char c = (char)key;
|
|
|
+ if (!shift) c += 32;
|
|
|
+ key_str = std::string(1, c);
|
|
|
+ } else if (key >= KEY_ZERO && key <= KEY_NINE) {
|
|
|
+ key_str = std::string(1, (char)key);
|
|
|
+ } else {
|
|
|
+ switch(key) {
|
|
|
+ case KEY_COMMA: key_str = ","; break;
|
|
|
+ case KEY_PERIOD: key_str = "."; break;
|
|
|
+ case KEY_SLASH: key_str = "/"; break;
|
|
|
+ case KEY_SEMICOLON: key_str = ";"; break;
|
|
|
+ case KEY_EQUAL: key_str = "="; break;
|
|
|
+ case KEY_MINUS: key_str = "-"; break;
|
|
|
+ case KEY_LEFT_BRACKET: key_str = "["; break;
|
|
|
+ case KEY_RIGHT_BRACKET: key_str = "]"; break;
|
|
|
+ case KEY_BACKSLASH: key_str = "\\"; break;
|
|
|
+ case KEY_APOSTROPHE: key_str = "'"; break;
|
|
|
+ case KEY_GRAVE: key_str = "`"; break;
|
|
|
+ case KEY_SPACE: key_str = "Space"; break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (key_str.empty()) return "";
|
|
|
+
|
|
|
+ if (ctrl) key_str = "C-" + key_str;
|
|
|
+ if (alt) key_str = "M-" + key_str;
|
|
|
+ if (shift) {
|
|
|
+ if (key_str == "Tab") key_str = "S-Tab";
|
|
|
+ }
|
|
|
+ return key_str;
|
|
|
+}
|
|
|
+
|
|
|
+void RaylibEditor::process_input() {
|
|
|
+ bool ctrl = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL);
|
|
|
+ bool alt = IsKeyDown(KEY_LEFT_ALT) || IsKeyDown(KEY_RIGHT_ALT);
|
|
|
+ bool shift = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
|
|
|
+
|
|
|
+ int key = GetKeyPressed();
|
|
|
+ while (key != 0) {
|
|
|
+ std::string lumacs_key = resolve_raylib_key(key, ctrl, alt, shift);
|
|
|
+ bool is_printable = (key >= 32 && key <= 126);
|
|
|
+ bool is_special = !is_printable || (key == KEY_SPACE);
|
|
|
+ bool should_handle = (ctrl || alt || is_special);
|
|
|
+
|
|
|
+ if (should_handle && !lumacs_key.empty()) {
|
|
|
+ bool handled_by_minibuffer = core_->minibuffer_manager().handle_key_event(lumacs_key);
|
|
|
+ if (!handled_by_minibuffer) {
|
|
|
+ KeyProcessingResult result = core_->keybinding_manager().process_key(lumacs_key);
|
|
|
+ if (result.type == KeyResult::Unbound) {
|
|
|
+ if (lumacs_key == "Return") core_->command_system().execute("newline", {});
|
|
|
+ else if (lumacs_key == "Backspace") core_->command_system().execute("delete-backward-char", {});
|
|
|
+ else if (lumacs_key == "Delete") core_->command_system().execute("delete-char", {});
|
|
|
+ else if (lumacs_key == "Tab") core_->command_system().execute("self-insert-command", {"\t"});
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ key = GetKeyPressed();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!ctrl && !alt) {
|
|
|
+ int char_code = GetCharPressed();
|
|
|
+ while (char_code != 0) {
|
|
|
+ if (char_code >= 32 && char_code <= 126) {
|
|
|
+ std::string text(1, (char)char_code);
|
|
|
+ bool handled_by_minibuffer = core_->minibuffer_manager().handle_key_event(text);
|
|
|
+ if (!handled_by_minibuffer) {
|
|
|
+ KeyProcessingResult result = core_->keybinding_manager().process_key(text);
|
|
|
+ if (result.type == KeyResult::Unbound) {
|
|
|
+ core_->command_system().execute("self-insert-command", {text});
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ char_code = GetCharPressed();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RaylibEditor::render() {
|
|
|
+ BeginDrawing();
|
|
|
+
|
|
|
+ auto theme = core_->theme_manager().active_theme();
|
|
|
+ ::Color bg = theme ? ToRLColor(theme->get_bg_color(ThemeElement::Normal)) : RL_Color(245, 245, 245);
|
|
|
+ ClearBackground(bg);
|
|
|
+
|
|
|
+ if (core_->window_manager().root_layout()) {
|
|
|
+ float mini_h = (float)line_height_ * 1.5f;
|
|
|
+ Rectangle editor_area = {0, 0, (float)GetScreenWidth(), (float)GetScreenHeight() - mini_h};
|
|
|
+ Rectangle mini_area = {0, (float)GetScreenHeight() - mini_h, (float)GetScreenWidth(), mini_h};
|
|
|
+
|
|
|
+ draw_layout(core_->window_manager().root_layout().get(), editor_area);
|
|
|
+ draw_minibuffer(mini_area);
|
|
|
+ }
|
|
|
+
|
|
|
+ EndDrawing();
|
|
|
+}
|
|
|
+
|
|
|
+void RaylibEditor::draw_layout(LayoutNode* node, Rectangle area) {
|
|
|
+ if (!node) return;
|
|
|
+
|
|
|
+ if (node->type == LayoutNode::Type::Leaf) {
|
|
|
+ if (node->window) {
|
|
|
+ draw_window(*node->window, area);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ Rectangle area1 = area;
|
|
|
+ Rectangle area2 = area;
|
|
|
+ auto theme = core_->theme_manager().active_theme();
|
|
|
+ ::Color border = theme ? ToRLColor(theme->get_fg_color(ThemeElement::WindowBorder)) : RL_Color(0,0,0);
|
|
|
+
|
|
|
+ if (node->type == LayoutNode::Type::HorizontalSplit) {
|
|
|
+ float h1 = area.height * node->ratio;
|
|
|
+ area1.height = h1;
|
|
|
+ area2.y += h1;
|
|
|
+ area2.height -= h1;
|
|
|
+ DrawLine((int)area.x, (int)area2.y, (int)area.width, (int)area2.y, border);
|
|
|
+ } else {
|
|
|
+ float w1 = area.width * node->ratio;
|
|
|
+ area1.width = w1;
|
|
|
+ area2.x += w1;
|
|
|
+ area2.width -= w1;
|
|
|
+ DrawLine((int)area2.x, (int)area.y, (int)area2.x, (int)area.height, border);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (node->child1) draw_layout(node->child1.get(), area1);
|
|
|
+ if (node->child2) draw_layout(node->child2.get(), area2);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RaylibEditor::draw_window(Window& window, Rectangle area) {
|
|
|
+ Rectangle content_area = area;
|
|
|
+ content_area.height -= line_height_;
|
|
|
+
|
|
|
+ int cols = (int)content_area.width / char_width_;
|
|
|
+ int lines = (int)content_area.height / line_height_;
|
|
|
+ if (cols < 1) cols = 1;
|
|
|
+ if (lines < 1) lines = 1;
|
|
|
+ window.set_viewport_size(cols, lines);
|
|
|
+
|
|
|
+ auto theme = core_->theme_manager().active_theme();
|
|
|
+ ::Color bg = theme ? ToRLColor(theme->get_bg_color(ThemeElement::Normal)) : RL_Color(255,255,255);
|
|
|
+ ::Color fg = theme ? ToRLColor(theme->get_fg_color(ThemeElement::Normal)) : RL_Color(0,0,0);
|
|
|
+
|
|
|
+ DrawRectangleRec(area, bg);
|
|
|
+
|
|
|
+ BeginScissorMode((int)content_area.x, (int)content_area.y, (int)content_area.width, (int)content_area.height);
|
|
|
+
|
|
|
+ auto range = window.visible_line_range();
|
|
|
+ float y = content_area.y;
|
|
|
+
|
|
|
+ for (size_t i = range.first; i < range.second; ++i) {
|
|
|
+ if (i < window.buffer().line_count()) {
|
|
|
+ std::string line = window.buffer().line(i);
|
|
|
+ DrawTextEx(font_, line.c_str(), {content_area.x, y}, (float)font_size_, 1.0f, fg);
|
|
|
+
|
|
|
+ if (i == window.cursor().line) {
|
|
|
+ float cx = content_area.x + (window.cursor().column * char_width_);
|
|
|
+ DrawRectangle((int)cx, (int)y, char_width_, line_height_, RL_Color(255, 0, 0, 128));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ y += line_height_;
|
|
|
+ }
|
|
|
+
|
|
|
+ EndScissorMode();
|
|
|
+
|
|
|
+ Rectangle modeline_rect = {area.x, area.y + area.height - line_height_, area.width, (float)line_height_};
|
|
|
+ draw_modeline(window, modeline_rect);
|
|
|
+}
|
|
|
+
|
|
|
+void RaylibEditor::draw_modeline(Window& window, Rectangle area) {
|
|
|
+ auto theme = core_->theme_manager().active_theme();
|
|
|
+ ::Color bg = theme ? ToRLColor(theme->get_bg_color(ThemeElement::StatusLine)) : RL_Color(200,200,200);
|
|
|
+ ::Color fg = theme ? ToRLColor(theme->get_fg_color(ThemeElement::StatusLine)) : RL_Color(0,0,0);
|
|
|
+
|
|
|
+ DrawRectangleRec(area, bg);
|
|
|
+
|
|
|
+ std::string mode_name = "Fundamental";
|
|
|
+ std::string buf_name = window.buffer().name();
|
|
|
+ bool modified = window.buffer().is_modified();
|
|
|
+
|
|
|
+ std::string text = fmt::format("{}{} ({}) L:{} C:{}",
|
|
|
+ modified ? "*" : "-", buf_name, mode_name,
|
|
|
+ window.cursor().line + 1, window.cursor().column);
|
|
|
+
|
|
|
+ DrawTextEx(font_, text.c_str(), {area.x + 5, area.y}, (float)font_size_, 1.0f, fg);
|
|
|
+}
|
|
|
+
|
|
|
+void RaylibEditor::draw_minibuffer(Rectangle area) {
|
|
|
+ auto theme = core_->theme_manager().active_theme();
|
|
|
+ ::Color bg = theme ? ToRLColor(theme->get_bg_color(ThemeElement::Normal)) : RL_Color(240,240,240);
|
|
|
+ ::Color fg = theme ? ToRLColor(theme->get_fg_color(ThemeElement::Normal)) : RL_Color(0,0,0);
|
|
|
+
|
|
|
+ DrawRectangleRec(area, bg);
|
|
|
+ DrawLine((int)area.x, (int)area.y, (int)area.width, (int)area.y, RL_Color(100,100,100));
|
|
|
+
|
|
|
+ std::string msg = core_->last_message();
|
|
|
+ if (!msg.empty()) {
|
|
|
+ DrawTextEx(font_, msg.c_str(), {area.x + 5, area.y}, (float)font_size_, 1.0f, fg);
|
|
|
+ } else if (core_->minibuffer_manager().is_active()) {
|
|
|
+ std::string prompt = core_->minibuffer_manager().get_prompt();
|
|
|
+ std::string input = core_->minibuffer_manager().get_input_buffer();
|
|
|
+ std::string full = prompt + input;
|
|
|
+
|
|
|
+ DrawTextEx(font_, full.c_str(), {area.x + 5, area.y}, (float)font_size_, 1.0f, fg);
|
|
|
+
|
|
|
+ int cursor_idx = core_->minibuffer_manager().get_cursor_position();
|
|
|
+ float prompt_width = MeasureTextEx(font_, prompt.c_str(), (float)font_size_, 1.0f).x;
|
|
|
+ float input_before_cursor = MeasureTextEx(font_, input.substr(0, cursor_idx).c_str(), (float)font_size_, 1.0f).x;
|
|
|
+
|
|
|
+ DrawRectangle((int)(area.x + 5 + prompt_width + input_before_cursor), (int)area.y, char_width_, line_height_, RL_Color(255, 0, 0, 128));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+std::unique_ptr<IEditorView> create_raylib_editor() {
|
|
|
+ return std::make_unique<RaylibEditor>();
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace lumacs
|