From 615cae687d4f43ad680bf352e1e28cc013e46550 Mon Sep 17 00:00:00 2001 From: apio Date: Sun, 11 Feb 2024 17:10:17 +0100 Subject: [PATCH 1/9] 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(); +} -- 2.34.1 From 88a6beff5a17b7b626b28a07996a40d3f11294d9 Mon Sep 17 00:00:00 2001 From: apio Date: Tue, 27 Feb 2024 20:11:14 +0100 Subject: [PATCH 2/9] editor: Add basic loading and saving --- editor/EditorWidget.cpp | 35 +++++++++++++++++++++++++++++++++-- editor/EditorWidget.h | 4 ++++ editor/main.cpp | 13 +++++++++++-- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/editor/EditorWidget.cpp b/editor/EditorWidget.cpp index 2481fe6a..094053db 100644 --- a/editor/EditorWidget.cpp +++ b/editor/EditorWidget.cpp @@ -31,6 +31,11 @@ Result EditorWidget::load_file(const os::Path& path) m_cursor = m_data.size(); + m_path = path; + + String title = TRY(String::format("Text Editor - %s"_sv, m_path.name().chars())); + ui::App::the().main_window()->set_title(title.view()); + TRY(recalculate_lines()); return {}; @@ -46,7 +51,9 @@ Result EditorWidget::handle_key_event(const ui::KeyEventRequest if (request.code == moon::K_Esc) { m_insert = false; - ui::App::the().main_window()->set_title("Text Editor"); + String title = TRY(String::from_cstring("Text Editor")); + if (!m_path.is_empty_path()) title = TRY(String::format("Text Editor - %s"_sv, m_path.name().chars())); + ui::App::the().main_window()->set_title(title.view()); } if (request.code == moon::K_Backspace) @@ -127,13 +134,37 @@ Result EditorWidget::handle_key_event(const ui::KeyEventRequest } case 'i': { m_insert = true; - ui::App::the().main_window()->set_title("Text Editor (insert)"); + String title = TRY(String::from_cstring("Text Editor (insert)")); + if (!m_path.is_empty_path()) title = TRY(String::format("Text Editor - %s (insert)"_sv, m_path.name().chars())); + ui::App::the().main_window()->set_title(title.view()); return ui::EventResult::DidHandle; } + case 'f': { + auto result = save_file(); + if (result.has_error()) + { + os::eprintln("TextEditor: failed to save file: %s", result.error_string()); + return ui::EventResult::DidNotHandle; + } + os::println("TextEditor: buffer saved to %s successfully", m_path.name().chars()); + return ui::EventResult::DidNotHandle; + } default: return ui::EventResult::DidNotHandle; } } +Result EditorWidget::save_file() +{ + if (m_path.is_empty_path()) + { + os::eprintln("TextEditor: no file to save buffer to!"); + return err(ENOENT); + } + + auto file = TRY(os::File::open(m_path, os::File::WriteOnly)); + return file->write(m_data); +} + Result EditorWidget::draw(ui::Canvas& canvas) { int visible_lines = canvas.height / m_font->height(); diff --git a/editor/EditorWidget.h b/editor/EditorWidget.h index 7b0730f8..418c269e 100644 --- a/editor/EditorWidget.h +++ b/editor/EditorWidget.h @@ -19,6 +19,8 @@ class EditorWidget : public ui::Widget Result load_file(const os::Path& path); + Result save_file(); + Result handle_key_event(const ui::KeyEventRequest& request) override; Result draw(ui::Canvas& canvas) override; @@ -43,6 +45,8 @@ class EditorWidget : public ui::Widget bool m_insert = false; + os::Path m_path { AT_FDCWD }; + void tick_cursor(); Result recalculate_lines(); diff --git a/editor/main.cpp b/editor/main.cpp index fdea5393..57d0012e 100644 --- a/editor/main.cpp +++ b/editor/main.cpp @@ -8,10 +8,19 @@ */ #include "EditorWidget.h" +#include #include -Result luna_main(int, char**) +Result luna_main(int argc, char** argv) { + StringView path; + + os::ArgumentParser parser; + parser.add_description("A graphical text editor"_sv); + parser.add_system_program_info("editor"_sv); + parser.add_positional_argument(path, "path", false); + parser.parse(argc, argv); + ui::App app; TRY(app.init()); @@ -21,7 +30,7 @@ Result luna_main(int, char**) app.set_main_window(window); auto* editor = TRY(make(ui::Font::default_font())); - // TRY(editor->load_file("/etc/skel/welcome")); + if (!path.is_empty()) TRY(editor->load_file(path)); window->set_main_widget(*editor); window->draw(); -- 2.34.1 From 2cf6549608edfd3215f78535e65fb50131eb1d00 Mon Sep 17 00:00:00 2001 From: apio Date: Wed, 6 Mar 2024 22:41:21 +0100 Subject: [PATCH 3/9] editor: Remove insert mode and use the arrow keys to navigate, plus Ctrl+S to save --- editor/EditorWidget.cpp | 127 +++++++++++++++++++--------------------- editor/EditorWidget.h | 2 - 2 files changed, 60 insertions(+), 69 deletions(-) diff --git a/editor/EditorWidget.cpp b/editor/EditorWidget.cpp index 094053db..7b5e619e 100644 --- a/editor/EditorWidget.cpp +++ b/editor/EditorWidget.cpp @@ -46,57 +46,8 @@ Result EditorWidget::handle_key_event(const ui::KeyEventRequest // Avoid handling "key released" events if (!request.pressed) return ui::EventResult::DidNotHandle; - if (m_insert) + if (request.code == moon::K_UpArrow) { - if (request.code == moon::K_Esc) - { - m_insert = false; - String title = TRY(String::from_cstring("Text Editor")); - if (!m_path.is_empty_path()) title = TRY(String::format("Text Editor - %s"_sv, m_path.name().chars())); - ui::App::the().main_window()->set_title(title.view()); - } - - 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; @@ -105,7 +56,9 @@ Result EditorWidget::handle_key_event(const ui::KeyEventRequest m_cursor_activated = true; return ui::EventResult::DidHandle; } - case 's': { + + if (request.code == moon::K_DownArrow) + { if (m_cursor_position.y + 1 < (int)m_lines.size()) m_cursor_position.y++; else return ui::EventResult::DidNotHandle; @@ -114,7 +67,9 @@ Result EditorWidget::handle_key_event(const ui::KeyEventRequest m_cursor_activated = true; return ui::EventResult::DidHandle; } - case 'a': { + + if (request.code == moon::K_LeftArrow) + { if (m_cursor > 0) m_cursor--; else return ui::EventResult::DidNotHandle; @@ -123,7 +78,9 @@ Result EditorWidget::handle_key_event(const ui::KeyEventRequest m_cursor_activated = true; return ui::EventResult::DidHandle; } - case 'd': { + + if (request.code == moon::K_RightArrow) + { if (m_cursor < m_data.size()) m_cursor++; else return ui::EventResult::DidNotHandle; @@ -132,25 +89,61 @@ Result EditorWidget::handle_key_event(const ui::KeyEventRequest m_cursor_activated = true; return ui::EventResult::DidHandle; } - case 'i': { - m_insert = true; - String title = TRY(String::from_cstring("Text Editor (insert)")); - if (!m_path.is_empty_path()) title = TRY(String::format("Text Editor - %s (insert)"_sv, m_path.name().chars())); - ui::App::the().main_window()->set_title(title.view()); - return ui::EventResult::DidHandle; - } - case 'f': { - auto result = save_file(); - if (result.has_error()) + + if (request.modifiers & ui::Mod_Ctrl) + { + switch (request.key) { - os::eprintln("TextEditor: failed to save file: %s", result.error_string()); + case 's': { + auto result = save_file(); + if (result.has_error()) + { + os::eprintln("TextEditor: failed to save file: %s", result.error_string()); + return ui::EventResult::DidNotHandle; + } + os::println("TextEditor: buffer saved to %s successfully", m_path.name().chars()); return ui::EventResult::DidNotHandle; } - os::println("TextEditor: buffer saved to %s successfully", m_path.name().chars()); - return ui::EventResult::DidNotHandle; + default: return ui::EventResult::DidNotHandle; + } } - default: return ui::EventResult::DidNotHandle; + + 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; } Result EditorWidget::save_file() diff --git a/editor/EditorWidget.h b/editor/EditorWidget.h index 418c269e..024b24b6 100644 --- a/editor/EditorWidget.h +++ b/editor/EditorWidget.h @@ -43,8 +43,6 @@ class EditorWidget : public ui::Widget OwnedPtr m_cursor_timer; bool m_cursor_activated = true; - bool m_insert = false; - os::Path m_path { AT_FDCWD }; void tick_cursor(); -- 2.34.1 From 0c8fe315db8ab84c9472b47bf45ead0d4865b982 Mon Sep 17 00:00:00 2001 From: apio Date: Thu, 14 Mar 2024 13:09:03 +0100 Subject: [PATCH 4/9] editor: Refuse to load non-regular file types --- editor/EditorWidget.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/editor/EditorWidget.cpp b/editor/EditorWidget.cpp index 7b5e619e..3bba0713 100644 --- a/editor/EditorWidget.cpp +++ b/editor/EditorWidget.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include EditorWidget::EditorWidget(SharedPtr font) : ui::Widget(), m_font(font) @@ -21,6 +22,15 @@ EditorWidget::EditorWidget(SharedPtr font) : ui::Widget(), m_font(font Result EditorWidget::load_file(const os::Path& path) { + struct stat st; + TRY(os::FileSystem::stat(path, st, true)); + + if (!S_ISREG(st.st_mode)) + { + os::eprintln("editor: not loading %s as it is not a regular file", path.name().chars()); + return {}; + } + os::eprintln("Loading file: %s", path.name().chars()); auto file = TRY(os::File::open_or_create(path, os::File::ReadOnly)); @@ -98,10 +108,10 @@ Result EditorWidget::handle_key_event(const ui::KeyEventRequest auto result = save_file(); if (result.has_error()) { - os::eprintln("TextEditor: failed to save file: %s", result.error_string()); + os::eprintln("editor: failed to save file: %s", result.error_string()); return ui::EventResult::DidNotHandle; } - os::println("TextEditor: buffer saved to %s successfully", m_path.name().chars()); + os::println("editor: buffer saved to %s successfully", m_path.name().chars()); return ui::EventResult::DidNotHandle; } default: return ui::EventResult::DidNotHandle; @@ -150,7 +160,7 @@ Result EditorWidget::save_file() { if (m_path.is_empty_path()) { - os::eprintln("TextEditor: no file to save buffer to!"); + os::eprintln("editor: no file to save buffer to!"); return err(ENOENT); } -- 2.34.1 From 43c0c801ae561d7665288dcfe1e53a24cbfd9ffb Mon Sep 17 00:00:00 2001 From: apio Date: Fri, 29 Mar 2024 20:16:40 +0100 Subject: [PATCH 5/9] libui: Add a TextInput base class to handle most input fields and add an InputField class for single-line inputs --- libui/CMakeLists.txt | 2 + libui/include/ui/InputField.h | 40 ++++++++++++ libui/include/ui/TextInput.h | 41 +++++++++++++ libui/src/InputField.cpp | 112 ++++++++++++++++++++++++++++++++++ libui/src/TextInput.cpp | 52 ++++++++++++++++ 5 files changed, 247 insertions(+) create mode 100644 libui/include/ui/InputField.h create mode 100644 libui/include/ui/TextInput.h create mode 100644 libui/src/InputField.cpp create mode 100644 libui/src/TextInput.cpp diff --git a/libui/CMakeLists.txt b/libui/CMakeLists.txt index 04dcfcdc..fb325b54 100644 --- a/libui/CMakeLists.txt +++ b/libui/CMakeLists.txt @@ -17,6 +17,8 @@ set(SOURCES src/Container.cpp src/Button.cpp src/Label.cpp + src/InputField.cpp + src/TextInput.cpp ) add_library(ui ${SOURCES}) diff --git a/libui/include/ui/InputField.h b/libui/include/ui/InputField.h new file mode 100644 index 00000000..bd3bab56 --- /dev/null +++ b/libui/include/ui/InputField.h @@ -0,0 +1,40 @@ +/** + * @file InputField.h + * @author apio (cloudapio.eu) + * @brief Single line text input widget. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#pragma once +#include +#include +#include + +namespace ui +{ + class InputField : public ui::TextInput + { + public: + InputField(SharedPtr font); + + Result handle_key_event(const ui::KeyEventRequest& request) override; + + Result draw(ui::Canvas& canvas) override; + + StringView data(); + + void on_submit(os::Function&& action) + { + m_on_submit_action = move(action); + m_has_on_submit_action = true; + } + + private: + SharedPtr m_font; + + os::Function m_on_submit_action; + bool m_has_on_submit_action { false }; + }; +} diff --git a/libui/include/ui/TextInput.h b/libui/include/ui/TextInput.h new file mode 100644 index 00000000..6d38b3a7 --- /dev/null +++ b/libui/include/ui/TextInput.h @@ -0,0 +1,41 @@ +/** + * @file TextInput.h + * @author apio (cloudapio.eu) + * @brief Base class for text inputs. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#pragma once +#include +#include +#include + +namespace ui +{ + class TextInput : public Widget + { + public: + TextInput(); + + virtual Result handle_key_event(const ui::KeyEventRequest& request) = 0; + + virtual Result draw(ui::Canvas& canvas) = 0; + + protected: + Buffer m_data; + + usize m_cursor { 0 }; + ui::Point m_cursor_position { 0, 0 }; + + OwnedPtr m_cursor_timer; + bool m_cursor_activated = true; + + void tick_cursor(); + void update_cursor(); + + Result delete_current_character(); + Result insert_character(char c); + }; +} diff --git a/libui/src/InputField.cpp b/libui/src/InputField.cpp new file mode 100644 index 00000000..a1e00eeb --- /dev/null +++ b/libui/src/InputField.cpp @@ -0,0 +1,112 @@ +/** + * @file InputField.cpp + * @author apio (cloudapio.eu) + * @brief Single line text input widget. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#include +#include +#include +#include +#include + +namespace ui +{ + InputField::InputField(SharedPtr font) : ui::TextInput(), m_font(font) + { + } + + Result InputField::handle_key_event(const ui::KeyEventRequest& request) + { + // Avoid handling "key released" events + if (!request.pressed) return ui::EventResult::DidNotHandle; + + if (request.code == moon::K_LeftArrow) + { + if (m_cursor > 0) m_cursor--; + else + return ui::EventResult::DidNotHandle; + update_cursor(); + return ui::EventResult::DidHandle; + } + + if (request.code == moon::K_RightArrow) + { + if (m_cursor < m_data.size()) m_cursor++; + else + return ui::EventResult::DidNotHandle; + update_cursor(); + return ui::EventResult::DidHandle; + } + + if (request.code == moon::K_Backspace) + { + if (m_cursor == 0) return ui::EventResult::DidNotHandle; + m_cursor--; + + delete_current_character(); + + update_cursor(); + + return ui::EventResult::DidHandle; + } + + if (request.letter == '\n') + { + if (m_has_on_submit_action) + { + m_on_submit_action(data()); + return ui::EventResult::DidHandle; + } + + return ui::EventResult::DidNotHandle; + } + + if (iscntrl(request.letter)) return ui::EventResult::DidNotHandle; + + if (m_cursor == m_data.size()) TRY(m_data.append_data((const u8*)&request.letter, 1)); + else + TRY(insert_character(request.letter)); + + m_cursor++; + + update_cursor(); + + return ui::EventResult::DidHandle; + } + + Result InputField::draw(ui::Canvas& canvas) + { + int visible_characters = canvas.width / m_font->width(); + + auto string = data(); + + 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_characters && j < characters_to_render; j++) + { + auto subcanvas = canvas.subcanvas({ j * m_font->width(), 0, m_font->width(), m_font->height() }); + m_font->render(buf[j], ui::WHITE, subcanvas); + } + + // Draw the cursor + if ((int)m_cursor < visible_characters && m_cursor_activated) + { + canvas.subcanvas({ (int)m_cursor * m_font->width(), 0, 1, m_font->height() }).fill(ui::WHITE); + } + + return {}; + } + + StringView InputField::data() + { + return StringView { (const char*)m_data.data(), m_data.size() }; + } +} diff --git a/libui/src/TextInput.cpp b/libui/src/TextInput.cpp new file mode 100644 index 00000000..0e47a84a --- /dev/null +++ b/libui/src/TextInput.cpp @@ -0,0 +1,52 @@ +/** + * @file TextInput.cpp + * @author apio (cloudapio.eu) + * @brief Base class for text inputs. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#include +#include + +namespace ui +{ + TextInput::TextInput() : Widget() + { + m_cursor_timer = os::Timer::create_repeating(500, [this]() { this->tick_cursor(); }).release_value(); + } + + void TextInput::update_cursor() + { + m_cursor_timer->restart(); + m_cursor_activated = true; + } + + Result TextInput::delete_current_character() + { + 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)); + + return {}; + } + + Result TextInput::insert_character(char c) + { + usize size = m_data.size() - m_cursor; + u8* slice = TRY(m_data.slice(m_cursor, size + 1)); + memmove(slice + 1, slice, size); + *slice = (u8)c; + + return {}; + } + + void TextInput::tick_cursor() + { + m_cursor_activated = !m_cursor_activated; + + window()->draw(); + } +} -- 2.34.1 From a06ecef08dbd3a6ca5fbde90fc6f84455a6440ec Mon Sep 17 00:00:00 2001 From: apio Date: Fri, 29 Mar 2024 20:17:12 +0100 Subject: [PATCH 6/9] editor: Use TextInput as a base class --- editor/EditorWidget.cpp | 42 ++++++++++------------------------------- editor/EditorWidget.h | 13 ++----------- editor/main.cpp | 2 +- 3 files changed, 13 insertions(+), 44 deletions(-) diff --git a/editor/EditorWidget.cpp b/editor/EditorWidget.cpp index 3bba0713..ad1b8ebc 100644 --- a/editor/EditorWidget.cpp +++ b/editor/EditorWidget.cpp @@ -14,9 +14,8 @@ #include #include -EditorWidget::EditorWidget(SharedPtr font) : ui::Widget(), m_font(font) +EditorWidget::EditorWidget(SharedPtr font) : ui::TextInput(), m_font(font) { - m_cursor_timer = os::Timer::create_repeating(500, [this]() { this->tick_cursor(); }).release_value(); recalculate_lines(); } @@ -44,7 +43,7 @@ Result EditorWidget::load_file(const os::Path& path) m_path = path; String title = TRY(String::format("Text Editor - %s"_sv, m_path.name().chars())); - ui::App::the().main_window()->set_title(title.view()); + window()->set_title(title.view()); TRY(recalculate_lines()); @@ -62,8 +61,7 @@ Result EditorWidget::handle_key_event(const ui::KeyEventRequest else return ui::EventResult::DidNotHandle; recalculate_cursor_index(); - m_cursor_timer->restart(); - m_cursor_activated = true; + update_cursor(); return ui::EventResult::DidHandle; } @@ -73,8 +71,7 @@ Result EditorWidget::handle_key_event(const ui::KeyEventRequest else return ui::EventResult::DidNotHandle; recalculate_cursor_index(); - m_cursor_timer->restart(); - m_cursor_activated = true; + update_cursor(); return ui::EventResult::DidHandle; } @@ -84,8 +81,7 @@ Result EditorWidget::handle_key_event(const ui::KeyEventRequest else return ui::EventResult::DidNotHandle; recalculate_cursor_position(); - m_cursor_timer->restart(); - m_cursor_activated = true; + update_cursor(); return ui::EventResult::DidHandle; } @@ -95,8 +91,7 @@ Result EditorWidget::handle_key_event(const ui::KeyEventRequest else return ui::EventResult::DidNotHandle; recalculate_cursor_position(); - m_cursor_timer->restart(); - m_cursor_activated = true; + update_cursor(); return ui::EventResult::DidHandle; } @@ -123,15 +118,11 @@ Result EditorWidget::handle_key_event(const ui::KeyEventRequest 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)); + delete_current_character(); TRY(recalculate_lines()); - m_cursor_timer->restart(); - m_cursor_activated = true; + update_cursor(); return ui::EventResult::DidHandle; } @@ -140,18 +131,12 @@ Result EditorWidget::handle_key_event(const ui::KeyEventRequest 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; - } + TRY(insert_character(request.letter)); m_cursor++; TRY(recalculate_lines()); - m_cursor_timer->restart(); - m_cursor_activated = true; + update_cursor(); return ui::EventResult::DidHandle; } @@ -234,13 +219,6 @@ Result EditorWidget::recalculate_lines() 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 }; diff --git a/editor/EditorWidget.h b/editor/EditorWidget.h index 024b24b6..3a543d92 100644 --- a/editor/EditorWidget.h +++ b/editor/EditorWidget.h @@ -10,9 +10,10 @@ #include #include #include +#include #include -class EditorWidget : public ui::Widget +class EditorWidget : public ui::TextInput { public: EditorWidget(SharedPtr font); @@ -28,8 +29,6 @@ class EditorWidget : public ui::Widget private: SharedPtr m_font; - Buffer m_data; - struct Line { usize begin; @@ -37,16 +36,8 @@ class EditorWidget : public ui::Widget }; Vector m_lines; - usize m_cursor { 0 }; - ui::Point m_cursor_position { 0, 0 }; - - OwnedPtr m_cursor_timer; - bool m_cursor_activated = true; - os::Path m_path { AT_FDCWD }; - void tick_cursor(); - Result recalculate_lines(); void recalculate_cursor_position(); void recalculate_cursor_index(); diff --git a/editor/main.cpp b/editor/main.cpp index 57d0012e..cca1edcf 100644 --- a/editor/main.cpp +++ b/editor/main.cpp @@ -30,8 +30,8 @@ Result luna_main(int argc, char** argv) app.set_main_window(window); auto* editor = TRY(make(ui::Font::default_font())); - if (!path.is_empty()) TRY(editor->load_file(path)); window->set_main_widget(*editor); + if (!path.is_empty()) TRY(editor->load_file(path)); window->draw(); -- 2.34.1 From ec44213f4e9c088f823abf2d68e59696fa1ff776 Mon Sep 17 00:00:00 2001 From: apio Date: Fri, 29 Mar 2024 20:17:51 +0100 Subject: [PATCH 7/9] terminal: Use widget->window() instead of the App's main window --- terminal/TerminalWidget.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/terminal/TerminalWidget.cpp b/terminal/TerminalWidget.cpp index d26d9b7a..11db8b85 100644 --- a/terminal/TerminalWidget.cpp +++ b/terminal/TerminalWidget.cpp @@ -40,7 +40,7 @@ Result TerminalWidget::init(char* const* args) m_font = ui::Font::default_font(); m_bold_font = ui::Font::default_bold_font(); - m_terminal_canvas = ui::App::the().main_window()->canvas(); + m_terminal_canvas = window()->canvas(); m_terminal_canvas.fill(ui::BLACK); m_cursor_timer = TRY(os::Timer::create_repeating(500, [this]() { this->tick_cursor(); })); @@ -102,7 +102,7 @@ Result TerminalWidget::process() if (did_draw) drawn++; } - if (drawn > 0) ui::App::the().main_window()->draw(); + if (drawn > 0) window()->draw(); return {}; } @@ -117,7 +117,7 @@ void TerminalWidget::tick_cursor() else erase_current_char(); - ui::App::the().main_window()->draw(); + window()->draw(); } void TerminalWidget::draw_glyph(wchar_t c, int x, int y) -- 2.34.1 From d6f7069589a62667311ec5337cec30844c2f259d Mon Sep 17 00:00:00 2001 From: apio Date: Fri, 29 Mar 2024 20:36:44 +0100 Subject: [PATCH 8/9] editor: Display only the basename of the current file in the window title --- editor/EditorWidget.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/editor/EditorWidget.cpp b/editor/EditorWidget.cpp index ad1b8ebc..833e7076 100644 --- a/editor/EditorWidget.cpp +++ b/editor/EditorWidget.cpp @@ -9,6 +9,7 @@ #include "EditorWidget.h" #include +#include #include #include #include @@ -42,7 +43,9 @@ Result EditorWidget::load_file(const os::Path& path) m_path = path; - String title = TRY(String::format("Text Editor - %s"_sv, m_path.name().chars())); + auto basename = TRY(PathParser::basename(m_path.name())); + + String title = TRY(String::format("Text Editor - %s"_sv, basename.chars())); window()->set_title(title.view()); TRY(recalculate_lines()); -- 2.34.1 From f3d9d4bcc0ed66ab74409a09126e0e181bb9cbd6 Mon Sep 17 00:00:00 2001 From: apio Date: Fri, 29 Mar 2024 20:41:16 +0100 Subject: [PATCH 9/9] editor: Fix creation of new files The editor is supposed to create files if they don't exist, however before this commit stat() would fail and exit load_file() before we even got to File::open_or_create(). --- editor/EditorWidget.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editor/EditorWidget.cpp b/editor/EditorWidget.cpp index 833e7076..901c3058 100644 --- a/editor/EditorWidget.cpp +++ b/editor/EditorWidget.cpp @@ -23,9 +23,9 @@ EditorWidget::EditorWidget(SharedPtr font) : ui::TextInput(), m_font(f Result EditorWidget::load_file(const os::Path& path) { struct stat st; - TRY(os::FileSystem::stat(path, st, true)); + auto rc = os::FileSystem::stat(path, st, true); - if (!S_ISREG(st.st_mode)) + if (!rc.has_error() && !S_ISREG(st.st_mode)) { os::eprintln("editor: not loading %s as it is not a regular file", path.name().chars()); return {}; -- 2.34.1