From d1537e1566c1c629a6dfc2de1de490f26f088d21 Mon Sep 17 00:00:00 2001 From: apio Date: Sun, 11 Feb 2024 17:10:17 +0100 Subject: [PATCH] editor: Add a basic text editor --- CMakeLists.txt | 1 + editor/CMakeLists.txt | 12 ++ editor/EditorWidget.cpp | 236 ++++++++++++++++++++++++++++++++++++++++ editor/EditorWidget.h | 51 +++++++++ editor/main.cpp | 30 +++++ 5 files changed, 330 insertions(+) create mode 100644 editor/CMakeLists.txt create mode 100644 editor/EditorWidget.cpp create mode 100644 editor/EditorWidget.h create mode 100644 editor/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 83061e36..2a2b2c74 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,3 +53,4 @@ add_subdirectory(tests) add_subdirectory(shell) add_subdirectory(wind) add_subdirectory(terminal) +add_subdirectory(editor) diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt new file mode 100644 index 00000000..161931d6 --- /dev/null +++ b/editor/CMakeLists.txt @@ -0,0 +1,12 @@ +set(SOURCES + main.cpp + EditorWidget.h + EditorWidget.cpp +) + +add_executable(editor ${SOURCES}) +target_compile_options(editor PRIVATE -Os ${COMMON_FLAGS} -Wno-write-strings) +add_dependencies(editor libc) +target_include_directories(editor PRIVATE ${LUNA_BASE}/usr/include ${CMAKE_CURRENT_LIST_DIR}) +target_link_libraries(editor PRIVATE os ui) +install(TARGETS editor DESTINATION ${LUNA_BASE}/usr/bin) diff --git a/editor/EditorWidget.cpp b/editor/EditorWidget.cpp new file mode 100644 index 00000000..2481fe6a --- /dev/null +++ b/editor/EditorWidget.cpp @@ -0,0 +1,236 @@ +/** + * @file EditorWidget.cpp + * @author apio (cloudapio.eu) + * @brief Multiline text editing widget. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#include "EditorWidget.h" +#include +#include +#include +#include + +EditorWidget::EditorWidget(SharedPtr font) : ui::Widget(), m_font(font) +{ + m_cursor_timer = os::Timer::create_repeating(500, [this]() { this->tick_cursor(); }).release_value(); + recalculate_lines(); +} + +Result EditorWidget::load_file(const os::Path& path) +{ + os::eprintln("Loading file: %s", path.name().chars()); + + auto file = TRY(os::File::open_or_create(path, os::File::ReadOnly)); + + m_data = TRY(file->read_all()); + + os::eprintln("Read %zu bytes.", m_data.size()); + + m_cursor = m_data.size(); + + TRY(recalculate_lines()); + + return {}; +} + +Result EditorWidget::handle_key_event(const ui::KeyEventRequest& request) +{ + // Avoid handling "key released" events + if (!request.pressed) return ui::EventResult::DidNotHandle; + + if (m_insert) + { + if (request.code == moon::K_Esc) + { + m_insert = false; + ui::App::the().main_window()->set_title("Text Editor"); + } + + if (request.code == moon::K_Backspace) + { + if (m_cursor == 0) return ui::EventResult::DidNotHandle; + m_cursor--; + + usize size = m_data.size() - m_cursor; + u8* slice = TRY(m_data.slice(m_cursor, size)); + memmove(slice, slice + 1, size - 1); + TRY(m_data.try_resize(m_data.size() - 1)); + + TRY(recalculate_lines()); + + m_cursor_timer->restart(); + m_cursor_activated = true; + + return ui::EventResult::DidHandle; + } + + if (request.letter != '\n' && iscntrl(request.letter)) return ui::EventResult::DidNotHandle; + + if (m_cursor == m_data.size()) TRY(m_data.append_data((const u8*)&request.letter, 1)); + else + { + usize size = m_data.size() - m_cursor; + u8* slice = TRY(m_data.slice(m_cursor, size + 1)); + memmove(slice + 1, slice, size); + *slice = request.letter; + } + + m_cursor++; + TRY(recalculate_lines()); + + m_cursor_timer->restart(); + m_cursor_activated = true; + + return ui::EventResult::DidHandle; + } + + switch (request.letter) + { + case 'w': { + if (m_cursor_position.y > 0) m_cursor_position.y--; + else + return ui::EventResult::DidNotHandle; + recalculate_cursor_index(); + m_cursor_timer->restart(); + m_cursor_activated = true; + return ui::EventResult::DidHandle; + } + case 's': { + if (m_cursor_position.y + 1 < (int)m_lines.size()) m_cursor_position.y++; + else + return ui::EventResult::DidNotHandle; + recalculate_cursor_index(); + m_cursor_timer->restart(); + m_cursor_activated = true; + return ui::EventResult::DidHandle; + } + case 'a': { + if (m_cursor > 0) m_cursor--; + else + return ui::EventResult::DidNotHandle; + recalculate_cursor_position(); + m_cursor_timer->restart(); + m_cursor_activated = true; + return ui::EventResult::DidHandle; + } + case 'd': { + if (m_cursor < m_data.size()) m_cursor++; + else + return ui::EventResult::DidNotHandle; + recalculate_cursor_position(); + m_cursor_timer->restart(); + m_cursor_activated = true; + return ui::EventResult::DidHandle; + } + case 'i': { + m_insert = true; + ui::App::the().main_window()->set_title("Text Editor (insert)"); + return ui::EventResult::DidHandle; + } + default: return ui::EventResult::DidNotHandle; + } +} + +Result EditorWidget::draw(ui::Canvas& canvas) +{ + int visible_lines = canvas.height / m_font->height(); + int visible_columns = canvas.width / m_font->width(); + + if ((usize)visible_lines > m_lines.size()) visible_lines = static_cast(m_lines.size()); + + for (int i = 0; i < visible_lines; i++) + { + auto line = m_lines[i]; + if (line.begin == line.end) continue; + + auto slice = TRY(m_data.slice(line.begin, line.end - line.begin)); + auto string = TRY( + String::from_string_view(StringView::from_fixed_size_cstring((const char*)slice, line.end - line.begin))); + + Utf8StringDecoder decoder(string.chars()); + wchar_t buf[4096]; + decoder.decode(buf, sizeof(buf)).release_value(); + + int characters_to_render = (int)wcslen(buf); + + for (int j = 0; j < visible_columns && j < characters_to_render; j++) + { + auto subcanvas = + canvas.subcanvas({ j * m_font->width(), i * m_font->height(), m_font->width(), m_font->height() }); + m_font->render(buf[j], ui::WHITE, subcanvas); + } + } + + // Draw the cursor + if (m_cursor_position.x < visible_columns && m_cursor_position.y < visible_lines && m_cursor_activated) + { + canvas + .subcanvas( + { m_cursor_position.x * m_font->width(), m_cursor_position.y * m_font->height(), 1, m_font->height() }) + .fill(ui::WHITE); + } + + return {}; +} + +Result EditorWidget::recalculate_lines() +{ + m_lines.clear(); + + Line l; + l.begin = 0; + for (usize i = 0; i < m_data.size(); i++) + { + if (m_data.data()[i] == '\n') + { + l.end = i; + TRY(m_lines.try_append(l)); + l.begin = i + 1; + } + } + + l.end = m_data.size(); + TRY(m_lines.try_append(l)); + + recalculate_cursor_position(); + + return {}; +} + +void EditorWidget::tick_cursor() +{ + m_cursor_activated = !m_cursor_activated; + + ui::App::the().main_window()->draw(); +} + +void EditorWidget::recalculate_cursor_position() +{ + if (m_cursor == 0) m_cursor_position = { 0, 0 }; + + for (int i = 0; i < (int)m_lines.size(); i++) + { + auto line = m_lines[i]; + if (m_cursor >= line.begin && m_cursor <= line.end) + { + m_cursor_position.x = (int)(m_cursor - line.begin); + m_cursor_position.y = i; + return; + } + } + + unreachable(); +} + +void EditorWidget::recalculate_cursor_index() +{ + m_cursor = m_lines[m_cursor_position.y].begin + m_cursor_position.x; + if (m_cursor > m_lines[m_cursor_position.y].end) + { + m_cursor = m_lines[m_cursor_position.y].end; + recalculate_cursor_position(); + } +} diff --git a/editor/EditorWidget.h b/editor/EditorWidget.h new file mode 100644 index 00000000..7b0730f8 --- /dev/null +++ b/editor/EditorWidget.h @@ -0,0 +1,51 @@ +/** + * @file EditorWidget.h + * @author apio (cloudapio.eu) + * @brief Multiline text editing widget. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#include +#include +#include +#include + +class EditorWidget : public ui::Widget +{ + public: + EditorWidget(SharedPtr font); + + Result load_file(const os::Path& path); + + Result handle_key_event(const ui::KeyEventRequest& request) override; + + Result draw(ui::Canvas& canvas) override; + + private: + SharedPtr m_font; + + Buffer m_data; + + struct Line + { + usize begin; + usize end; + }; + Vector m_lines; + + usize m_cursor { 0 }; + ui::Point m_cursor_position { 0, 0 }; + + OwnedPtr m_cursor_timer; + bool m_cursor_activated = true; + + bool m_insert = false; + + void tick_cursor(); + + Result recalculate_lines(); + void recalculate_cursor_position(); + void recalculate_cursor_index(); +}; diff --git a/editor/main.cpp b/editor/main.cpp new file mode 100644 index 00000000..fdea5393 --- /dev/null +++ b/editor/main.cpp @@ -0,0 +1,30 @@ +/** + * @file main.cpp + * @author apio (cloudapio.eu) + * @brief Graphical text editor. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#include "EditorWidget.h" +#include + +Result luna_main(int, char**) +{ + ui::App app; + TRY(app.init()); + + auto* window = TRY(ui::Window::create(ui::Rect { 200, 300, 600, 600 })); + window->set_background(ui::Color::from_rgb(40, 40, 40)); + window->set_title("Text Editor"); + app.set_main_window(window); + + auto* editor = TRY(make(ui::Font::default_font())); + // TRY(editor->load_file("/etc/skel/welcome")); + window->set_main_widget(*editor); + + window->draw(); + + return app.run(); +}