260 lines
6.9 KiB
C++
260 lines
6.9 KiB
C++
/**
|
|
* @file EditorWidget.cpp
|
|
* @author apio (cloudapio.eu)
|
|
* @brief Multiline text editing widget.
|
|
*
|
|
* @copyright Copyright (c) 2024, the Luna authors.
|
|
*
|
|
*/
|
|
|
|
#include "EditorWidget.h"
|
|
#include <ctype.h>
|
|
#include <luna/PathParser.h>
|
|
#include <luna/RefString.h>
|
|
#include <luna/Utf8.h>
|
|
#include <os/File.h>
|
|
#include <os/FileSystem.h>
|
|
#include <ui/App.h>
|
|
#include <ui/Dialog.h>
|
|
|
|
EditorWidget::EditorWidget(SharedPtr<ui::Font> font) : ui::TextInput(), m_font(font)
|
|
{
|
|
recalculate_lines();
|
|
}
|
|
|
|
Result<void> EditorWidget::load_file(const os::Path& path)
|
|
{
|
|
struct stat st;
|
|
auto rc = os::FileSystem::stat(path, st, true);
|
|
|
|
if (!rc.has_error() && !S_ISREG(st.st_mode))
|
|
{
|
|
auto message = TRY(RefString::format("%s is not a regular file", path.name().chars()));
|
|
ui::Dialog::show_message("Error", message.view());
|
|
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 = TRY(String::from_string_view(path.name()));
|
|
|
|
auto basename = TRY(PathParser::basename(m_path.view()));
|
|
|
|
String title = TRY(String::format("Text Editor - %s"_sv, basename.chars()));
|
|
window()->set_title(title.view());
|
|
|
|
TRY(recalculate_lines());
|
|
|
|
return {};
|
|
}
|
|
|
|
Result<ui::EventResult> 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();
|
|
update_cursor();
|
|
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();
|
|
update_cursor();
|
|
return ui::EventResult::DidHandle;
|
|
}
|
|
|
|
if (request.code == moon::K_LeftArrow)
|
|
{
|
|
if (m_cursor > 0) m_cursor--;
|
|
else
|
|
return ui::EventResult::DidNotHandle;
|
|
recalculate_cursor_position();
|
|
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;
|
|
recalculate_cursor_position();
|
|
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();
|
|
|
|
TRY(recalculate_lines());
|
|
|
|
update_cursor();
|
|
|
|
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
|
|
TRY(insert_character(request.letter));
|
|
|
|
m_cursor++;
|
|
TRY(recalculate_lines());
|
|
|
|
update_cursor();
|
|
|
|
return ui::EventResult::DidHandle;
|
|
}
|
|
|
|
Result<void> EditorWidget::save_file_as()
|
|
{
|
|
ui::Dialog::show_input_dialog(
|
|
"Save file as...", "Please enter the path to save this file to:", [this](StringView path) {
|
|
m_path = String::from_string_view(path).release_value();
|
|
auto rc = save_file();
|
|
if (rc.has_error())
|
|
{
|
|
os::eprintln("Failed to save file %s: %s", m_path.chars(), rc.error_string());
|
|
ui::Dialog::show_message("Error", "Failed to save file");
|
|
}
|
|
else
|
|
{
|
|
auto basename = PathParser::basename(m_path.view()).release_value();
|
|
|
|
String title = String::format("Text Editor - %s"_sv, basename.chars()).release_value();
|
|
window()->set_title(title.view());
|
|
}
|
|
});
|
|
|
|
return {};
|
|
}
|
|
|
|
Result<void> EditorWidget::save_file()
|
|
{
|
|
if (m_path.is_empty())
|
|
{
|
|
TRY(save_file_as());
|
|
return {};
|
|
}
|
|
|
|
auto file = TRY(os::File::open_or_create(m_path.view(), os::File::WriteOnly));
|
|
return file->write(m_data);
|
|
}
|
|
|
|
Result<void> 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<int>(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<void> 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::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();
|
|
}
|
|
}
|