/** * @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(); } }