genode/repos/gems/src/app/text_area/dialog.cc

737 lines
16 KiB
C++

/*
* \brief Text dialog
* \author Norman Feske
* \date 2020-01-14
*/
/*
* Copyright (C) 2020 Genode Labs GmbH
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
/* local includes */
#include <dialog.h>
using namespace Text_area;
enum {
CODEPOINT_BACKSPACE = 8, CODEPOINT_NEWLINE = 10,
CODEPOINT_UP = 0xf700, CODEPOINT_DOWN = 0xf701,
CODEPOINT_LEFT = 0xf702, CODEPOINT_RIGHT = 0xf703,
CODEPOINT_HOME = 0xf729, CODEPOINT_INSERT = 0xf727,
CODEPOINT_DELETE = 0xf728, CODEPOINT_END = 0xf72b,
CODEPOINT_PAGEUP = 0xf72c, CODEPOINT_PAGEDOWN = 0xf72d,
};
static bool movement_codepoint(Codepoint code)
{
auto v = code.value;
return (v == CODEPOINT_UP) || (v == CODEPOINT_DOWN) ||
(v == CODEPOINT_LEFT) || (v == CODEPOINT_RIGHT) ||
(v == CODEPOINT_HOME) || (v == CODEPOINT_END) ||
(v == CODEPOINT_PAGEUP) || (v == CODEPOINT_PAGEDOWN);
}
static bool shift_key(Input::Keycode key)
{
return (key == Input::KEY_LEFTSHIFT) || (key == Input::KEY_RIGHTSHIFT);
}
static bool control_key(Input::Keycode key)
{
return (key == Input::KEY_LEFTCTRL) || (key == Input::KEY_RIGHTCTRL);
}
template <typename T>
static void swap(T &v1, T &v2) { auto tmp = v1; v1 = v2; v2 = tmp; };
template <typename FN>
void Dialog::Selection::for_each_selected_line(FN const &fn) const
{
if (!defined())
return;
unsigned start_y = start->y.value, end_y = end->y.value;
if (end_y < start_y)
swap(start_y, end_y);
if (end_y < start_y)
return;
for (unsigned i = start_y; i <= end_y; i++)
fn(Text::Index { i }, (i == end_y));
}
template <typename FN>
void Dialog::Selection::with_selection_at_line(Text::Index y, Line const &line,
FN const &fn) const
{
if (!defined())
return;
Line::Index start_x = start->x, end_x = end->x;
Text::Index start_y = start->y, end_y = end->y;
if (end_y.value < start_y.value) {
swap(start_x, end_x);
swap(start_y, end_y);
}
if (y.value < start_y.value || y.value > end_y.value)
return;
if (y.value > start_y.value)
start_x = Line::Index { 0 };
if (y.value < end_y.value)
end_x = line.upper_bound();
if (start_x.value > end_x.value)
swap(start_x, end_x);
fn(start_x, end_x.value - start_x.value);
}
void Dialog::Selection::gen_selected_line(Xml_generator &xml,
Text::Index y, Line const &line) const
{
with_selection_at_line(y, line, [&] (Line::Index const start_x, const unsigned n) {
xml.node("selection", [&] () {
xml.attribute("at", start_x.value);
xml.attribute("length", n);
});
});
}
void Dialog::produce_xml(Xml_generator &xml)
{
auto gen_line = [&] (Text::Index at, Line const &line)
{
xml.node("hbox", [&] () {
xml.attribute("name", at.value - _scroll.y.value);
xml.node("float", [&] () {
xml.attribute("north", "yes");
xml.attribute("south", "yes");
xml.attribute("west", "yes");
xml.node("label", [&] () {
xml.attribute("font", "monospace/regular");
xml.attribute("text", String<512>(line));
if (_cursor.y.value == at.value)
xml.node("cursor", [&] () {
xml.attribute("name", "cursor");
xml.attribute("at", _cursor.x.value); });
if (_hovered_position.constructed())
if (_hovered_position->y.value == at.value)
xml.node("cursor", [&] () {
xml.attribute("name", "hover");
xml.attribute("style", "hover");
xml.attribute("at", _hovered_position->x.value); });
_selection.gen_selected_line(xml, at, line);
});
});
});
};
xml.node("frame", [&] () {
xml.node("button", [&] () {
xml.attribute("name", "text");
if (_text_hovered)
xml.attribute("hovered", "yes");
xml.node("float", [&] () {
xml.attribute("north", "yes");
xml.attribute("east", "yes");
xml.attribute("west", "yes");
xml.node("vbox", [&] () {
Dynamic_array<Line>::Range const range { .at = _scroll.y,
.length = _max_lines };
_text.for_each(range, gen_line);
});
});
});
});
}
void Dialog::_delete_selection()
{
if (!_editable)
return;
if (!_selection.defined())
return;
_modification_count++;
/*
* Clear all characters within the selection
*/
unsigned num_lines = 0;
Text::Index first_y { 0 };
_selection.for_each_selected_line([&] (Text::Index const y, bool) {
_text.apply(y, [&] (Line &line) {
_selection.with_selection_at_line(y, line,
[&] (Line::Index x, unsigned n) {
for (unsigned i = 0; i < n; i++) {
line.destruct(Line::Index { x.value });
bool const cursor_right_of_deleted_character =
(_cursor.y.value == y.value) && (_cursor.x.value > x.value);
if (cursor_right_of_deleted_character)
_cursor.x.value--;
}
});
});
if (num_lines == 0)
first_y = y;
num_lines++;
});
/*
* Remove all selected lines, joining the remaining characters at the
* bounds of the selection.
*/
if (num_lines > 1) {
Text::Index const next_y { first_y.value + 1 };
while (--num_lines) {
bool const cursor_at_deleted_line = (_cursor.y.value == next_y.value);
bool const cursor_below_deleted_line = (_cursor.y.value > next_y.value);
_text.apply(first_y, [&] (Line &first) {
if (cursor_at_deleted_line)
_cursor = { .x = first.upper_bound(),
.y = first_y };
_text.apply(next_y, [&] (Line &next) {
_move_characters(next, first); });
});
_text.destruct(next_y);
if (cursor_below_deleted_line)
_cursor.y.value--;
}
}
_selection.clear();
}
void Dialog::_insert_printable(Codepoint code)
{
_tie_cursor_to_end_of_line();
_text.apply(_cursor.y, [&] (Line &line) {
line.insert(_cursor.x, Character(code)); });
_cursor.x.value++;
}
void Dialog::_handle_printable(Codepoint code)
{
if (!_editable)
return;
_modification_count++;
_delete_selection();
_insert_printable(code);
}
void Dialog::_move_characters(Line &from, Line &to)
{
/* move all characters of line 'from' to the end of line 'to' */
Line::Index const first { 0 };
while (from.exists(first)) {
from.apply(first, [&] (Codepoint &code) {
to.append(code); });
from.destruct(first);
}
}
void Dialog::_handle_backspace()
{
if (!_editable)
return;
_modification_count++;
/* eat backspace when deleting a selection */
if (_selection.defined()) {
_delete_selection();
return;
}
if (_cursor.x.value > 0) {
_cursor.x.value--;
_text.apply(_cursor.y, [&] (Line &line) {
line.destruct(_cursor.x); });
return;
}
if (_cursor.y.value == 0)
return;
/* join line with previous line */
Text::Index const prev_y { _cursor.y.value - 1 };
_text.apply(prev_y, [&] (Line &prev_line) {
_cursor.x = prev_line.upper_bound();
_text.apply(_cursor.y, [&] (Line &line) {
_move_characters(line, prev_line); });
});
_text.destruct(_cursor.y);
_cursor.y = prev_y;
}
void Dialog::_handle_delete()
{
if (!_editable)
return;
_modification_count++;
/* eat delete when deleting a selection */
if (_selection.defined()) {
_delete_selection();
return;
}
if (_end_of_text())
return;
_handle_right();
_handle_backspace();
}
void Dialog::_handle_newline()
{
if (!_editable)
return;
_modification_count++;
_delete_selection();
/* create new line at cursor position */
Text::Index const new_y { _cursor.y.value + 1 };
_text.insert(new_y, _alloc);
/* take the characters after the cursor to the new line */
_text.apply(_cursor.y, [&] (Line &line) {
_text.apply(new_y, [&] (Line &new_line) {
while (line.exists(_cursor.x)) {
line.apply(_cursor.x, [&] (Codepoint code) {
new_line.append(code); });
line.destruct(_cursor.x);
}
});
});
_cursor.y = new_y;
_cursor.x.value = 0;
}
void Dialog::_handle_left()
{
_tie_cursor_to_end_of_line();
if (_cursor.x.value == 0) {
if (_cursor.y.value > 0) {
_cursor.y.value--;
_text.apply(_cursor.y, [&] (Line &line) {
_cursor.x = line.upper_bound(); });
}
} else {
_cursor.x.value--;
}
}
void Dialog::_handle_right()
{
if (!_cursor_at_end_of_line()) {
_cursor.x.value++;
return;
}
if (!_cursor_at_last_line()) {
_cursor.x.value = 0;
_cursor.y.value++;
}
}
void Dialog::_handle_up()
{
if (_cursor.y.value > 0)
_cursor.y.value--;
}
void Dialog::_handle_down()
{
if (_cursor.y.value + 1 < _text.upper_bound().value)
_cursor.y.value++;
}
void Dialog::_handle_pageup()
{
if (_max_lines != ~0U) {
for (unsigned i = 0; i < _max_lines; i++)
_handle_up();
} else {
_cursor.y.value = 0;
}
}
void Dialog::_handle_pagedown()
{
if (_max_lines != ~0U) {
for (unsigned i = 0; i < _max_lines; i++)
_handle_down();
} else {
_cursor.y.value = _text.upper_bound().value;
}
}
void Dialog::_handle_home()
{
_cursor.x.value = 0;
}
void Dialog::_handle_end()
{
_text.apply(_cursor.y, [&] (Line &line) {
_cursor.x = line.upper_bound(); });
}
void Dialog::handle_input_event(Input::Event const &event)
{
bool update_dialog = false;
Position const orig_cursor = _cursor;
auto cursor_to_hovered_position = [&] ()
{
if (_hovered_position.constructed()) {
_cursor.x = _hovered_position->x;
_cursor.y = _hovered_position->y;
update_dialog = true;
}
};
event.handle_press([&] (Input::Keycode key, Codepoint code) {
bool key_has_visible_effect = true;
if (shift_key(key)) {
_shift = true;
if (!_selection.defined()) {
_selection.start.construct(_cursor);
_selection.end.destruct();
}
}
if (control_key(key))
_control = true;
if (!_control) {
if (!_shift && movement_codepoint(code))
_selection.clear();
if (_printable(code)) {
_handle_printable(code);
}
else if (code.value == CODEPOINT_BACKSPACE) { _handle_backspace(); }
else if (code.value == CODEPOINT_DELETE) { _handle_delete(); }
else if (code.value == CODEPOINT_NEWLINE) { _handle_newline(); }
else if (code.value == CODEPOINT_LEFT) { _handle_left(); }
else if (code.value == CODEPOINT_UP) { _handle_up(); }
else if (code.value == CODEPOINT_DOWN) { _handle_down(); }
else if (code.value == CODEPOINT_RIGHT) { _handle_right(); }
else if (code.value == CODEPOINT_PAGEDOWN) { _handle_pagedown(); }
else if (code.value == CODEPOINT_PAGEUP) { _handle_pageup(); }
else if (code.value == CODEPOINT_HOME) { _handle_home(); }
else if (code.value == CODEPOINT_END) { _handle_end(); }
else if (code.value == CODEPOINT_INSERT) { _trigger_paste.trigger_paste(); }
else {
key_has_visible_effect = false;
}
if (_shift && movement_codepoint(code))
_selection.end.construct(_cursor);
}
if (_control) {
if (code.value == 'c')
_trigger_copy.trigger_copy();
if (code.value == 'x') {
_trigger_copy.trigger_copy();
_delete_selection();
}
if (code.value == 'v')
_trigger_paste.trigger_paste();
if (code.value == 's')
_trigger_save.trigger_save();
}
if (key_has_visible_effect)
update_dialog = true;
bool const click = (key == Input::BTN_LEFT);
if (click && _hovered_position.constructed()) {
if (_shift)
_selection.end.construct(*_hovered_position);
else
_selection.start.construct(*_hovered_position);
_drag = true;
}
bool const middle_click = (key == Input::BTN_MIDDLE);
if (middle_click) {
cursor_to_hovered_position();
_trigger_paste.trigger_paste();
}
});
if (_drag && _hovered_position.constructed()) {
_selection.end.construct(*_hovered_position);
update_dialog = true;
}
bool const clack = event.key_release(Input::BTN_LEFT);
if (clack) {
cursor_to_hovered_position();
_drag = false;
if (_selection.defined())
_trigger_copy.trigger_copy();
}
event.handle_release([&] (Input::Keycode key) {
if (shift_key(key)) _shift = false;
if (control_key(key)) _control = false;
});
bool const all_lines_visible =
(_max_lines == ~0U) || (_text.upper_bound().value <= _max_lines);
if (!all_lines_visible) {
event.handle_wheel([&] (int, int y) {
/* scroll at granulatory of 1/5th of vertical view size */
y *= max(1U, _max_lines / 5);
if (y < 0)
_scroll.y.value += -y;
if (y > 0)
_scroll.y.value -= min((int)_scroll.y.value, y);
update_dialog = true;
});
}
/* adjust scroll position */
if (all_lines_visible) {
_scroll.y.value = 0;
} else if (orig_cursor != _cursor) {
/* ensure that the cursor remains visible */
if (_cursor.y.value > 0)
if (_scroll.y.value > _cursor.y.value - 1)
_scroll.y.value = _cursor.y.value - 1;
if (_cursor.y.value == 0)
_scroll.y.value = 0;
if (_scroll.y.value + _max_lines < _cursor.y.value + 2)
_scroll.y.value = _cursor.y.value - _max_lines + 2;
}
_clamp_scroll_position_to_upper_bound();
if (update_dialog)
rom_session.trigger_update();
}
void Dialog::handle_hover(Xml_node const &hover)
{
Constructible<Position> orig_pos { };
if (_hovered_position.constructed())
orig_pos.construct(*_hovered_position);
_hovered_position.destruct();
auto with_hovered_line = [&] (Xml_node node)
{
Text::Index const y {
node.attribute_value("name", _text.upper_bound().value)
+ _scroll.y.value };
_text.apply(y, [&] (Line const &line) {
Line::Index const max_x = line.upper_bound();
_hovered_position.construct(max_x, y);
node.with_sub_node("float", [&] (Xml_node node) {
node.with_sub_node("label", [&] (Xml_node node) {
Line::Index const x {
node.attribute_value("at", max_x.value) };
_hovered_position.construct(x, y);
});
});
});
};
bool const hover_changed =
(orig_pos.constructed() != _hovered_position.constructed());
bool const position_changed = orig_pos.constructed()
&& _hovered_position.constructed()
&& (*orig_pos != *_hovered_position);
bool const orig_text_hovered = _text_hovered;
_text_hovered = false;
hover.with_sub_node("frame", [&] (Xml_node node) {
node.with_sub_node("button", [&] (Xml_node node) {
_text_hovered = true;
node.with_sub_node("float", [&] (Xml_node node) {
node.with_sub_node("vbox", [&] (Xml_node node) {
node.with_sub_node("hbox", [&] (Xml_node node) {
with_hovered_line(node); }); }); }); }); });
if (hover_changed || position_changed || (_text_hovered != orig_text_hovered))
rom_session.trigger_update();
}
Dialog::Dialog(Entrypoint &ep, Ram_allocator &ram, Region_map &rm,
Allocator &alloc, Trigger_copy &trigger_copy,
Trigger_paste &trigger_paste, Trigger_save &trigger_save)
:
Xml_producer("dialog"),
rom_session(ep, ram, rm, *this),
_alloc(alloc),
_trigger_copy(trigger_copy),
_trigger_paste(trigger_paste),
_trigger_save(trigger_save)
{
clear();
}
void Dialog::clear()
{
Text::Index const first { 0 };
while (_text.exists(first))
_text.destruct(first);
_cursor.x.value = 0;
_cursor.y.value = 0;
}
void Dialog::insert_at_cursor_position(Codepoint c)
{
if (_printable(c)) {
_insert_printable(c);
_modification_count++;
return;
}
if (c.value == CODEPOINT_NEWLINE)
_handle_newline();
}
void Dialog::gen_clipboard_content(Xml_generator &xml) const
{
if (!_selection.defined())
return;
auto for_each_selected_character = [&] (auto fn)
{
_selection.for_each_selected_line([&] (Text::Index const y, bool const last) {
_text.apply(y, [&] (Line const &line) {
_selection.with_selection_at_line(y, line, [&] (Line::Index x, unsigned n) {
for (unsigned i = 0; i < n; i++)
line.apply(Line::Index { x.value + i }, [&] (Codepoint c) {
fn(c); }); }); });
if (!last)
fn(Codepoint{'\n'});
});
};
for_each_selected_character([&] (Codepoint c) {
String<10> const utf8(c);
if (utf8.valid())
xml.append_sanitized(utf8.string(), utf8.length() - 1);
});
}