/** * @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 #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) { 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)); m_data = TRY(file->read_all()); os::eprintln("Read %zu bytes.", m_data.size()); 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 {}; } Result EditorWidget::handle_key_event(const ui::KeyEventRequest& request) { // Avoid handling "key released" events if (!request.pressed) return ui::EventResult::DidNotHandle; if (request.code == moon::K_UpArrow) { 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; } 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; recalculate_cursor_index(); m_cursor_timer->restart(); m_cursor_activated = true; return ui::EventResult::DidHandle; } if (request.code == moon::K_LeftArrow) { 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; } if (request.code == moon::K_RightArrow) { 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; } if (request.modifiers & ui::Mod_Ctrl) { switch (request.key) { case 's': { auto result = save_file(); if (result.has_error()) { os::eprintln("editor: failed to save file: %s", result.error_string()); return ui::EventResult::DidNotHandle; } os::println("editor: buffer saved to %s successfully", m_path.name().chars()); 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() { if (m_path.is_empty_path()) { os::eprintln("editor: 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(); 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(); } }