diff --git a/repos/gems/recipes/src/text_area/content.mk b/repos/gems/recipes/src/text_area/content.mk
new file mode 100644
index 000000000..28780d322
--- /dev/null
+++ b/repos/gems/recipes/src/text_area/content.mk
@@ -0,0 +1,3 @@
+SRC_DIR := src/app/text_area
+
+include $(GENODE_DIR)/repos/base/recipes/src/content.inc
diff --git a/repos/gems/recipes/src/text_area/hash b/repos/gems/recipes/src/text_area/hash
new file mode 100644
index 000000000..d1e25e651
--- /dev/null
+++ b/repos/gems/recipes/src/text_area/hash
@@ -0,0 +1 @@
+2020-02-12 ca4f34ab1f8c172a367b4c955920a466dbb3ab18
diff --git a/repos/gems/recipes/src/text_area/used_apis b/repos/gems/recipes/src/text_area/used_apis
new file mode 100644
index 000000000..52dfa42c4
--- /dev/null
+++ b/repos/gems/recipes/src/text_area/used_apis
@@ -0,0 +1,9 @@
+base
+os
+vfs
+report_session
+file_system_session
+timer_session
+nitpicker_session
+input_session
+framebuffer_session
diff --git a/repos/gems/run/text_area.run b/repos/gems/run/text_area.run
new file mode 100644
index 000000000..c255810a5
--- /dev/null
+++ b/repos/gems/run/text_area.run
@@ -0,0 +1,152 @@
+create_boot_directory
+
+import_from_depot [depot_user]/src/[base_src] \
+ [depot_user]/pkg/[drivers_interactive_pkg] \
+ [depot_user]/pkg/fonts_fs \
+ [depot_user]/src/init \
+ [depot_user]/src/report_rom \
+ [depot_user]/src/nitpicker \
+ [depot_user]/src/libc \
+ [depot_user]/src/libpng \
+ [depot_user]/src/zlib \
+ [depot_user]/src/vfs_import
+
+install_config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
+
+set fd [open [run_dir]/genode/focus w]
+puts $fd " \"/>"
+close $fd
+
+build { app/text_area app/menu_view }
+
+build_boot_image { text_area sandbox.lib.so vfs.lib.so menu_view menu_view_styles.tar }
+
+run_genode_until forever
diff --git a/repos/gems/src/app/text_area/child_state.h b/repos/gems/src/app/text_area/child_state.h
new file mode 100644
index 000000000..49e04ea9d
--- /dev/null
+++ b/repos/gems/src/app/text_area/child_state.h
@@ -0,0 +1,124 @@
+/*
+ * \brief Runtime state of a child hosted in the runtime subsystem
+ * \author Norman Feske
+ * \date 2018-09-03
+ */
+
+/*
+ * Copyright (C) 2018 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.
+ */
+
+#ifndef _CHILD_STATE_H_
+#define _CHILD_STATE_H_
+
+/* Genode includes */
+#include
+#include
+#include
+#include
+#include
+
+/* local includes */
+#include "types.h"
+
+namespace Text_area { struct Child_state; }
+
+struct Text_area::Child_state : Noncopyable
+{
+ private:
+
+ using Start_name = String<128>;
+
+ Registry::Element _element;
+
+ Start_name const _name;
+
+ Ram_quota const _initial_ram_quota;
+ Cap_quota const _initial_cap_quota;
+
+ Ram_quota _ram_quota = _initial_ram_quota;
+ Cap_quota _cap_quota = _initial_cap_quota;
+
+ struct Version { unsigned value; } _version { 0 };
+
+ public:
+
+ /**
+ * Constructor
+ *
+ * \param ram_quota initial RAM quota
+ * \param cap_quota initial capability quota
+ */
+ Child_state(Registry ®istry, Start_name const &name,
+ Ram_quota ram_quota, Cap_quota cap_quota)
+ :
+ _element(registry, *this),
+ _name(name),
+ _initial_ram_quota(ram_quota), _initial_cap_quota(cap_quota)
+ { }
+
+ void trigger_restart()
+ {
+ _version.value++;
+ _ram_quota = _initial_ram_quota;
+ _cap_quota = _initial_cap_quota;
+ }
+
+ void gen_start_node_version(Xml_generator &xml) const
+ {
+ if (_version.value)
+ xml.attribute("version", _version.value);
+ }
+
+ void gen_start_node_content(Xml_generator &xml) const
+ {
+ xml.attribute("name", _name);
+
+ gen_start_node_version(xml);
+
+ xml.attribute("caps", _cap_quota.value);
+ xml.node("resource", [&] () {
+ xml.attribute("name", "RAM");
+ Number_of_bytes const bytes(_ram_quota.value);
+ xml.attribute("quantum", String<64>(bytes)); });
+ }
+
+ /**
+ * Adapt runtime state information to the child
+ *
+ * This method responds to RAM and cap-resource requests by increasing
+ * the resource quotas as needed.
+ *
+ * \param child child node of the runtime'r state report
+ * \return true if runtime must be reconfigured so that the changes
+ * can take effect
+ */
+ bool apply_child_state_report(Xml_node child)
+ {
+ bool result = false;
+
+ if (child.attribute_value("name", Start_name()) != _name)
+ return false;
+
+ if (child.has_sub_node("ram") && child.sub_node("ram").has_attribute("requested")) {
+ _ram_quota.value *= 2;
+ result = true;
+ }
+
+ if (child.has_sub_node("caps") && child.sub_node("caps").has_attribute("requested")) {
+ _cap_quota.value += 100;
+ result = true;
+ }
+
+ return result;
+ }
+
+ Ram_quota ram_quota() const { return _ram_quota; }
+
+ Start_name name() const { return _name; }
+};
+
+#endif /* _CHILD_STATE_H_ */
diff --git a/repos/gems/src/app/text_area/dialog.cc b/repos/gems/src/app/text_area/dialog.cc
new file mode 100644
index 000000000..610ebdb3e
--- /dev/null
+++ b/repos/gems/src/app/text_area/dialog.cc
@@ -0,0 +1,734 @@
+/*
+ * \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
+
+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
+static void swap(T &v1, T &v2) { auto tmp = v1; v1 = v2; v2 = tmp; };
+
+
+template
+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
+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::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);
+ }
+
+ 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 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);
+ });
+}
diff --git a/repos/gems/src/app/text_area/dialog.h b/repos/gems/src/app/text_area/dialog.h
new file mode 100644
index 000000000..0022210e9
--- /dev/null
+++ b/repos/gems/src/app/text_area/dialog.h
@@ -0,0 +1,254 @@
+/*
+ * \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.
+ */
+
+#ifndef _DIALOG_H_
+#define _DIALOG_H_
+
+/* Genode includes */
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* local includes */
+#include
+#include
+#include
+
+namespace Text_area { struct Dialog; }
+
+
+struct Text_area::Dialog : private Dynamic_rom_session::Xml_producer
+{
+ public:
+
+ Dynamic_rom_session rom_session;
+
+ struct Trigger_copy : Interface, Noncopyable
+ {
+ virtual void trigger_copy() = 0;
+ };
+
+ struct Trigger_paste : Interface, Noncopyable
+ {
+ virtual void trigger_paste() = 0;
+ };
+
+ struct Trigger_save : Interface, Noncopyable
+ {
+ virtual void trigger_save() = 0;
+ };
+
+ private:
+
+ Allocator &_alloc;
+
+ Trigger_copy &_trigger_copy;
+ Trigger_paste &_trigger_paste;
+ Trigger_save &_trigger_save;
+
+ struct Character : Codepoint
+ {
+ Character(Codepoint &codepoint) : Codepoint(codepoint) { }
+
+ void print(Output &out) const
+ {
+ if (value == '"')
+ Genode::print(out, """);
+ else if (value == 9)
+ Genode::print(out, " ");
+ else
+ Codepoint::print(out);
+ }
+ };
+
+ using Line = Dynamic_array;
+ using Text = Dynamic_array;
+
+ Text _text { _alloc };
+
+ struct Position
+ {
+ Line::Index x;
+ Text::Index y;
+
+ Position(Line::Index x, Text::Index y) : x(x), y(y) { }
+
+ Position(Position const &other) : x(other.x), y(other.y) { }
+
+ bool operator != (Position const &other) const
+ {
+ return (x.value != other.x.value) || (y.value != other.y.value);
+ }
+ };
+
+ Position _cursor { { 0 }, { 0 } };
+
+ Position _scroll { { 0 }, { 0 } };
+
+ Constructible _hovered_position { };
+
+ unsigned _max_lines = ~0U;
+
+ bool _editable = false;
+
+ unsigned _modification_count = 0;
+
+ struct Selection
+ {
+ Constructible start;
+ Constructible end;
+
+ void clear()
+ {
+ start.destruct();
+ end .destruct();
+ }
+
+ bool defined() const
+ {
+ return start.constructed() && end.constructed() && (*start != *end);
+ }
+
+ template
+ void for_each_selected_line(FN const &) const;
+
+ template
+ void with_selection_at_line(Text::Index y, Line const &, FN const &) const;
+
+ /* generate dialog model */
+ void gen_selected_line(Xml_generator &, Text::Index, Line const &) const;
+ };
+
+ bool _drag = false;
+ bool _shift = false;
+ bool _control = false;
+ bool _text_hovered = false;
+
+ Selection _selection { };
+
+ void produce_xml(Xml_generator &xml) override;
+
+ static bool _printable(Codepoint code)
+ {
+ if (!code.valid())
+ return false;
+
+ if (code.value == '\t')
+ return true;
+
+ return (code.value >= 0x20 && code.value < 0xf000);
+ }
+
+ bool _cursor_at_last_line() const
+ {
+ return (_cursor.y.value + 1 >= _text.upper_bound().value);
+ }
+
+ bool _cursor_at_end_of_line() const
+ {
+ bool result = false;
+ _text.apply(_cursor.y, [&] (Line &line) {
+ result = (_cursor.x.value >= line.upper_bound().value); });
+ return result;
+ }
+
+ void _tie_cursor_to_end_of_line()
+ {
+ _text.apply(_cursor.y, [&] (Line &line) {
+ if (_cursor.x.value > line.upper_bound().value)
+ _cursor.x = line.upper_bound(); });
+ }
+
+ bool _end_of_text() const
+ {
+ return _cursor_at_last_line() && _cursor_at_end_of_line();
+ }
+
+ void _clamp_scroll_position_to_upper_bound()
+ {
+ if (_max_lines != ~0U)
+ if (_scroll.y.value + _max_lines > _text.upper_bound().value)
+ _scroll.y.value = max(_text.upper_bound().value, _max_lines)
+ - _max_lines;
+ }
+
+ void _move_characters(Line &, Line &);
+ void _delete_selection();
+ void _insert_printable(Codepoint);
+ void _handle_printable(Codepoint);
+ void _handle_backspace();
+ void _handle_delete();
+ void _handle_newline();
+ void _handle_left();
+ void _handle_right();
+ void _handle_up();
+ void _handle_down();
+ void _handle_pageup();
+ void _handle_pagedown();
+ void _handle_home();
+ void _handle_end();
+
+ public:
+
+ Dialog(Entrypoint &, Ram_allocator &, Region_map &, Allocator &,
+ Trigger_copy &, Trigger_paste &, Trigger_save &);
+
+ void editable(bool editable) { _editable = editable; }
+
+ unsigned modification_count() const { return _modification_count; }
+
+ void max_lines(unsigned max_lines) { _max_lines = max_lines; }
+
+ void handle_input_event(Input::Event const &);
+
+ void handle_hover(Xml_node const &hover);
+
+ void clear();
+
+ void append_newline()
+ {
+ _text.append(_alloc);
+ }
+
+ void append_character(Codepoint c)
+ {
+ if (_printable(c)) {
+ Text::Index const y { _text.upper_bound().value - 1 };
+ _text.apply(y, [&] (Line &line) {
+ line.append(c); });
+ }
+ }
+
+ /* insert character and advance cursor */
+ void insert_at_cursor_position(Codepoint);
+
+ void gen_clipboard_content(Xml_generator &xml) const;
+
+ template
+ void for_each_character(FN const &fn) const
+ {
+ _text.for_each([&] (Text::Index at, Line const &line) {
+ line.for_each([&] (Line::Index, Character c) {
+ fn(c); });
+
+ if (at.value + 1 < _text.upper_bound().value)
+ fn(Codepoint{'\n'});
+ });
+ }
+};
+
+#endif /* _DIALOG_H_ */
diff --git a/repos/gems/src/app/text_area/dynamic_array.h b/repos/gems/src/app/text_area/dynamic_array.h
new file mode 100644
index 000000000..38961b96f
--- /dev/null
+++ b/repos/gems/src/app/text_area/dynamic_array.h
@@ -0,0 +1,194 @@
+/*
+ * \brief Dynamically growing array
+ * \author Norman Feske
+ * \date 2020-01-12
+ */
+
+/*
+ * 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.
+ */
+
+#ifndef _DYNAMIC_ARRAY_H_
+#define _DYNAMIC_ARRAY_H_
+
+/* Genode includes */
+#include
+
+namespace Text_area {
+
+ using namespace Genode;
+
+ template
+ struct Dynamic_array;
+}
+
+
+template
+struct Text_area::Dynamic_array
+{
+ public:
+
+ struct Index { unsigned value; };
+
+ private:
+
+ Allocator &_alloc;
+
+ using Element = Constructible;
+
+ Element *_array = nullptr;
+
+ unsigned _capacity = 0;
+ unsigned _upper_bound = 0; /* index after last used element */
+
+ bool _index_valid(Index at) const
+ {
+ return (at.value < _upper_bound) && _array[at.value].constructed();
+ }
+
+ /*
+ * Noncopyable
+ */
+ Dynamic_array(Dynamic_array const &other);
+ void operator = (Dynamic_array const &);
+
+ public:
+
+ /**
+ * Moving constructor
+ */
+ Dynamic_array(Dynamic_array &other)
+ :
+ _alloc(other._alloc), _array(other._array),
+ _capacity(other._capacity), _upper_bound(other._upper_bound)
+ {
+ other._array = nullptr;
+ other._capacity = 0;
+ other._upper_bound = 0;
+ }
+
+ Dynamic_array(Allocator &alloc) : _alloc(alloc) { }
+
+ ~Dynamic_array()
+ {
+ if (!_array)
+ return;
+
+ clear();
+
+ _alloc.free(_array, _capacity*sizeof(Element));
+ }
+
+ void clear()
+ {
+ if (_upper_bound > 0)
+ for (unsigned i = _upper_bound; i > 0; i--)
+ destruct(Index{i - 1});
+ }
+
+ template
+ void insert(Index at, ARGS &&... args)
+ {
+ /* grow array if index exceeds current capacity or if it's full */
+ if (at.value >= _capacity || _upper_bound == _capacity) {
+
+ size_t const new_capacity =
+ 2 * max(_capacity, max(8U, at.value));
+
+ Element *new_array = nullptr;
+ try {
+ (void)_alloc.alloc(sizeof(Element)*new_capacity, &new_array);
+
+ for (unsigned i = 0; i < new_capacity; i++)
+ construct_at(&new_array[i]);
+ }
+ catch (... /* Out_of_ram, Out_of_caps */ ) { throw; }
+
+ if (_array) {
+ for (unsigned i = 0; i < _upper_bound; i++)
+ new_array[i].construct(*_array[i]);
+
+ _alloc.free(_array, sizeof(Element)*_capacity);
+ }
+
+ _array = new_array;
+ _capacity = new_capacity;
+ }
+
+ /* make room for new element */
+ if (_upper_bound > 0)
+ for (unsigned i = _upper_bound; i > at.value; i--)
+ _array[i].construct(*_array[i - 1]);
+
+ _array[at.value].construct(args...);
+
+ _upper_bound = max(at.value + 1, _upper_bound + 1);
+ }
+
+ template
+ void append(ARGS &&... args) { insert(Index{_upper_bound}, args...); }
+
+ bool exists(Index at) const { return _index_valid(at); }
+
+ Index upper_bound() const { return Index { _upper_bound }; }
+
+ void destruct(Index at)
+ {
+ if (!_index_valid(at))
+ return;
+
+ _array[at.value].destruct();
+
+ if (_upper_bound > 0)
+ for (unsigned i = at.value; i < _upper_bound - 1; i++)
+ _array[i].construct(*_array[i + 1]);
+
+ _upper_bound--;
+ _array[_upper_bound].destruct();
+ }
+
+ template
+ void apply(Index at, FN const &fn)
+ {
+ if (_index_valid(at))
+ fn(*_array[at.value]);
+ }
+
+ template
+ void apply(Index at, FN const &fn) const
+ {
+ if (_index_valid(at))
+ fn(*_array[at.value]);
+ }
+
+ struct Range { Index at; unsigned length; };
+
+ template
+ void for_each(Range range, FN const &fn) const
+ {
+ unsigned const first = range.at.value;
+ unsigned const limit = min(_upper_bound, first + range.length);
+
+ for (unsigned i = first; i < limit; i++)
+ if (_array[i].constructed())
+ fn(Index{i}, *_array[i]);
+ }
+
+ template
+ void for_each(FN const &fn) const
+ {
+ for_each(Range { .at = { 0U }, .length = ~0U }, fn);
+ }
+
+ void print(Output &out) const
+ {
+ for (unsigned i = 0; i < _upper_bound; i++)
+ if (_array[i].constructed())
+ Genode::print(out, *_array[i]);
+ }
+};
+
+#endif /* _DYNAMIC_ARRAY_H_ */
diff --git a/repos/gems/src/app/text_area/input_event_handler.h b/repos/gems/src/app/text_area/input_event_handler.h
new file mode 100644
index 000000000..7bb6386d8
--- /dev/null
+++ b/repos/gems/src/app/text_area/input_event_handler.h
@@ -0,0 +1,28 @@
+/*
+ * \brief Interface for handling input events
+ * \author Norman Feske
+ * \date 2018-05-02
+ */
+
+/*
+ * Copyright (C) 2018 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.
+ */
+
+#ifndef _INPUT_EVENT_HANDLER_H_
+#define _INPUT_EVENT_HANDLER_H_
+
+/* Genode includes */
+#include
+#include
+
+namespace Nitpicker { struct Input_event_handler; }
+
+struct Nitpicker::Input_event_handler : Genode::Interface
+{
+ virtual void handle_input_event(Input::Event const &) = 0;
+};
+
+#endif /* _INPUT_EVENT_HANDLER_H_ */
diff --git a/repos/gems/src/app/text_area/main.cc b/repos/gems/src/app/text_area/main.cc
new file mode 100644
index 000000000..8a94d2f44
--- /dev/null
+++ b/repos/gems/src/app/text_area/main.cc
@@ -0,0 +1,525 @@
+/*
+ * \brief Simple text viewer and editor
+ * \author Norman Feske
+ * \date 2020-01-12
+ */
+
+/*
+ * 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.
+ */
+
+/* Genode includes */
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* local includes */
+#include
+#include
+#include
+#include
+#include
+
+namespace Text_area { struct Main; }
+
+struct Text_area::Main : Sandbox::Local_service_base::Wakeup,
+ Sandbox::State_handler,
+ Nitpicker::Input_event_handler,
+ Dialog::Trigger_copy, Dialog::Trigger_paste,
+ Dialog::Trigger_save
+{
+ Env &_env;
+
+ Heap _heap { _env.ram(), _env.rm() };
+
+ Attached_rom_dataspace _config { _env, "config" };
+
+ Root_directory _vfs { _env, _heap, _config.xml().sub_node("vfs") };
+
+ unsigned _min_width = 0;
+ unsigned _min_height = 0;
+
+ Registry _children { };
+
+ Child_state _menu_view_child_state { _children, "menu_view",
+ Ram_quota { 4*1024*1024 },
+ Cap_quota { 200 } };
+ /**
+ * Sandbox::State_handler
+ */
+ void handle_sandbox_state() override
+ {
+ /* obtain current sandbox state */
+ Buffered_xml state(_heap, "state", [&] (Xml_generator &xml) {
+ _sandbox.generate_state_report(xml);
+ });
+
+ bool reconfiguration_needed = false;
+
+ state.with_xml_node([&] (Xml_node state) {
+ state.for_each_sub_node("child", [&] (Xml_node const &child) {
+ if (_menu_view_child_state.apply_child_state_report(child))
+ reconfiguration_needed = true; }); });
+
+ if (reconfiguration_needed)
+ _update_sandbox_config();
+ }
+
+ Sandbox _sandbox { _env, *this };
+
+ typedef Sandbox::Local_service Nitpicker_service;
+
+ Nitpicker_service _nitpicker_service { _sandbox, *this };
+
+ typedef Sandbox::Local_service Rom_service;
+
+ Rom_service _rom_service { _sandbox, *this };
+
+ typedef Sandbox::Local_service Report_service;
+
+ Report_service _report_service { _sandbox, *this };
+
+ void _handle_hover(Xml_node const &node)
+ {
+ if (!node.has_sub_node("dialog"))
+ _dialog.handle_hover(Xml_node(""));
+
+ node.with_sub_node("dialog", [&] (Xml_node const &dialog) {
+ _dialog.handle_hover(dialog); });
+ }
+
+ Report::Session_component::Xml_handler
+ _hover_handler { *this, &Main::_handle_hover };
+
+ Dialog _dialog { _env.ep(), _env.ram(), _env.rm(), _heap, *this, *this, *this };
+
+ Constructible _saved_reporter { };
+
+ struct Saved_version { unsigned value; } _saved_version { 0 };
+
+ /*
+ * The dialog's modification count at the time of last saving
+ */
+ struct Modification_count { unsigned value; } _saved_modification_count { 0 };
+
+ bool _modified() const
+ {
+ return _dialog.modification_count() != _saved_modification_count.value;
+ }
+
+ void _generate_saved_report()
+ {
+ if (!_saved_reporter.constructed())
+ return;
+
+ _saved_reporter->generate([&] (Xml_generator &xml) {
+ xml.attribute("version", _saved_version.value);
+ if (_modified())
+ xml.attribute("modified", "yes");
+ });
+ }
+
+ void _generate_sandbox_config(Xml_generator &xml) const
+ {
+ xml.node("report", [&] () {
+ xml.attribute("child_ram", "yes");
+ xml.attribute("child_caps", "yes");
+ xml.attribute("delay_ms", 20*1000);
+ });
+ xml.node("parent-provides", [&] () {
+
+ auto service_node = [&] (char const *name) {
+ xml.node("service", [&] () {
+ xml.attribute("name", name); }); };
+
+ service_node("ROM");
+ service_node("CPU");
+ service_node("PD");
+ service_node("LOG");
+ service_node("File_system");
+ service_node("Nitpicker");
+ service_node("Timer");
+ service_node("Report");
+ });
+
+ xml.node("start", [&] () {
+ _menu_view_child_state.gen_start_node_content(xml);
+
+ xml.node("config", [&] () {
+ xml.attribute("xpos", "100");
+ xml.attribute("ypos", "50");
+
+ if (_min_width) xml.attribute("width", _min_width);
+ if (_min_height) xml.attribute("height", _min_height);
+
+ xml.node("report", [&] () {
+ xml.attribute("hover", "yes"); });
+
+ xml.node("libc", [&] () {
+ xml.attribute("stderr", "/dev/log"); });
+
+ xml.node("vfs", [&] () {
+ xml.node("tar", [&] () {
+ xml.attribute("name", "menu_view_styles.tar"); });
+ xml.node("dir", [&] () {
+ xml.attribute("name", "dev");
+ xml.node("log", [&] () { });
+ });
+ xml.node("dir", [&] () {
+ xml.attribute("name", "fonts");
+ xml.node("fs", [&] () {
+ xml.attribute("label", "fonts");
+ });
+ });
+ });
+ });
+
+ xml.node("route", [&] () {
+
+ xml.node("service", [&] () {
+ xml.attribute("name", "ROM");
+ xml.attribute("label", "dialog");
+ xml.node("local", [&] () { });
+ });
+
+ xml.node("service", [&] () {
+ xml.attribute("name", "Report");
+ xml.attribute("label", "hover");
+ xml.node("local", [&] () { });
+ });
+
+ xml.node("service", [&] () {
+ xml.attribute("name", "Nitpicker");
+ xml.node("local", [&] () { });
+ });
+
+ xml.node("service", [&] () {
+ xml.attribute("name", "File_system");
+ xml.attribute("label", "fonts");
+ xml.node("parent", [&] () {
+ xml.attribute("label", "fonts"); });
+ });
+
+ xml.node("any-service", [&] () {
+ xml.node("parent", [&] () { }); });
+ });
+ });
+ }
+
+ /**
+ * Sandbox::Local_service_base::Wakeup interface
+ */
+ void wakeup_local_service() override
+ {
+ _rom_service.for_each_requested_session([&] (Rom_service::Request &request) {
+
+ if (request.label == "menu_view -> dialog")
+ request.deliver_session(_dialog.rom_session);
+ else
+ request.deny();
+ });
+
+ _report_service.for_each_requested_session([&] (Report_service::Request &request) {
+
+ if (request.label == "menu_view -> hover") {
+ Report::Session_component &session = *new (_heap)
+ Report::Session_component(_env, _hover_handler,
+ _env.ep(),
+ request.resources, "", request.diag);
+ request.deliver_session(session);
+ }
+ });
+
+ _report_service.for_each_session_to_close([&] (Report::Session_component &session) {
+
+ destroy(_heap, &session);
+ return Report_service::Close_response::CLOSED;
+ });
+
+ _nitpicker_service.for_each_requested_session([&] (Nitpicker_service::Request &request) {
+
+ Nitpicker::Session_component &session = *new (_heap)
+ Nitpicker::Session_component(_env, *this, _env.ep(),
+ request.resources, "", request.diag);
+
+ request.deliver_session(session);
+ });
+
+ _nitpicker_service.for_each_upgraded_session([&] (Nitpicker::Session_component &session,
+ Session::Resources const &amount) {
+ session.upgrade(amount);
+ return Nitpicker_service::Upgrade_response::CONFIRMED;
+ });
+
+ _nitpicker_service.for_each_session_to_close([&] (Nitpicker::Session_component &session) {
+
+ destroy(_heap, &session);
+ return Nitpicker_service::Close_response::CLOSED;
+ });
+ }
+
+ /**
+ * Nitpicker::Input_event_handler interface
+ */
+ void handle_input_event(Input::Event const &event) override
+ {
+ bool const orig_modified = _modified();
+
+ _dialog.handle_input_event(event);
+
+ if (_modified() != orig_modified)
+ _generate_saved_report();
+ }
+
+ Directory::Path _path() const
+ {
+ return _config.xml().attribute_value("path", Directory::Path());
+ }
+
+ void _watch(bool enabled)
+ {
+ _watch_handler.conditional(enabled, _vfs, _path(), *this, &Main::_handle_watch);
+ }
+
+ bool _editable() const { return !_watch_handler.constructed(); }
+
+ void _load()
+ {
+ struct Max_line_len_exceeded : Exception { };
+
+ try {
+ File_content content(_heap, _vfs, _path(), File_content::Limit{1024*1024});
+
+ enum { MAX_LINE_LEN = 1000 };
+ typedef String Content_line;
+
+ _dialog.clear();
+ content.for_each_line([&] (Content_line const &line) {
+
+ if (line.length() == Content_line::capacity()) {
+ warning("maximum line length ", (size_t)MAX_LINE_LEN, " exceeded");
+ throw Max_line_len_exceeded();
+ }
+
+ _dialog.append_newline();
+
+ for (Utf8_ptr utf8(line.string()); utf8.complete(); utf8 = utf8.next())
+ _dialog.append_character(utf8.codepoint());
+ });
+
+ }
+ catch (...) {
+ warning("failed to load file ", _path());
+ _dialog.clear();
+ }
+
+ _dialog.rom_session.trigger_update();
+ }
+
+ Constructible> _watch_handler { };
+
+ void _handle_watch() { _load(); }
+
+ /*
+ * Copy
+ */
+
+ Constructible _clipboard_reporter { };
+
+ /**
+ * Dialog::Trigger_copy interface
+ */
+ void trigger_copy() override
+ {
+ if (!_clipboard_reporter.constructed())
+ return;
+
+ _clipboard_reporter->generate([&] (Xml_generator &xml) {
+ _dialog.gen_clipboard_content(xml); });
+ }
+
+ /*
+ * Paste
+ */
+
+ Constructible _clipboard_rom { };
+
+ enum { PASTE_BUFFER_SIZE = 64*1024 };
+ struct Paste_buffer { char buffer[PASTE_BUFFER_SIZE]; } _paste_buffer { };
+
+ /**
+ * Dialog::Trigger_paste interface
+ */
+ void trigger_paste() override
+ {
+ if (!_editable())
+ return;
+
+ if (!_clipboard_rom.constructed())
+ return;
+
+ _clipboard_rom->update();
+
+ _paste_buffer = { };
+
+ /* leave last byte as zero-termination in tact */
+ size_t const max_len = sizeof(_paste_buffer.buffer) - 1;
+ size_t const len =
+ _clipboard_rom->xml().decoded_content(_paste_buffer.buffer, max_len);
+
+ if (len == max_len) {
+ warning("clipboard content exceeds paste buffer");
+ return;
+ }
+
+ for (Utf8_ptr utf8(_paste_buffer.buffer); utf8.complete(); utf8 = utf8.next())
+ _dialog.insert_at_cursor_position(utf8.codepoint());
+
+ _dialog.rom_session.trigger_update();
+ }
+
+ /*
+ * Save
+ */
+
+ void _save_to_file(Directory::Path const &path)
+ {
+ bool write_error = false;
+
+ try {
+ New_file new_file(_vfs, path);
+
+ auto write = [&] (char const *cstring)
+ {
+ switch (new_file.append(cstring, strlen(cstring))) {
+ case New_file::Append_result::OK: break;
+ case New_file::Append_result::WRITE_ERROR:
+ write_error = true;
+ break;
+ }
+ };
+
+ Buffered_output<1024, decltype(write)> output(write);
+
+ _dialog.for_each_character([&] (Codepoint c) { print(output, c); });
+ }
+ catch (New_file::Create_failed) {
+ error("file creation failed while saving file"); }
+
+ if (write_error) {
+ error("write error while saving ", _path());
+ return;
+ }
+
+ _saved_modification_count.value = _dialog.modification_count();
+
+ _generate_saved_report();
+ }
+
+ /**
+ * Dialog::Trigger_save interface
+ */
+ void trigger_save() override
+ {
+ if (!_editable())
+ return;
+
+ _saved_version.value++;
+ _save_to_file(_path());
+ }
+
+ bool _initial_config = true;
+
+ void _handle_config()
+ {
+ _config.update();
+
+ Xml_node const config = _config.xml();
+
+ _min_width = config.attribute_value("min_width", 0U);
+ _min_height = config.attribute_value("min_height", 0U);
+
+ bool const copy_enabled = config.attribute_value("copy", false);
+ bool const paste_enabled = config.attribute_value("paste", false);
+
+ _clipboard_reporter.conditional(copy_enabled, _env, "clipboard", "clipboard");
+ _clipboard_rom .conditional(paste_enabled, _env, "clipboard");
+
+ _dialog.max_lines(config.attribute_value("max_lines", ~0U));
+
+ _watch(config.attribute_value("watch", false));
+
+ _dialog.editable(_editable());
+
+ if (_editable()) {
+ bool const orig_saved_reporter_enabled = _saved_reporter.constructed();
+
+ config.with_sub_node("report", [&] (Xml_node const &node) {
+ _saved_reporter.conditional(node.attribute_value("saved", false),
+ _env, "saved", "saved"); });
+
+ bool const saved_report_out_of_date =
+ !orig_saved_reporter_enabled && _saved_reporter.constructed();
+
+ Saved_version const orig_saved_version = _saved_version;
+
+ config.with_sub_node("save", [&] (Xml_node const &node) {
+ _saved_version.value =
+ node.attribute_value("version", _saved_version.value); });
+
+ bool const saved_version_changed =
+ (_saved_version.value != orig_saved_version.value);
+
+ if (saved_version_changed || saved_report_out_of_date) {
+
+ if (!_initial_config)
+ _save_to_file(_path());
+ else
+ _generate_saved_report();
+ }
+ }
+
+ _initial_config = false;
+ }
+
+ Signal_handler _config_handler {
+ _env.ep(), *this, &Main::_handle_config };
+
+ void _update_sandbox_config()
+ {
+ Buffered_xml const config { _heap, "config", [&] (Xml_generator &xml) {
+ _generate_sandbox_config(xml); } };
+
+ config.with_xml_node([&] (Xml_node const &config) {
+ _sandbox.apply_config(config); });
+ }
+
+ Main(Env &env)
+ :
+ _env(env)
+ {
+ /*
+ * The '_load' must be performed before '_handle_config' because
+ * '_handle_config' may call '_save_to_file' if the contains a
+ * node.
+ */
+ _load();
+
+ _config.sigh(_config_handler);
+ _handle_config();
+ _update_sandbox_config();
+ }
+};
+
+
+void Component::construct(Genode::Env &env) { static Text_area::Main main(env); }
+
diff --git a/repos/gems/src/app/text_area/new_file.h b/repos/gems/src/app/text_area/new_file.h
new file mode 100644
index 000000000..f34228763
--- /dev/null
+++ b/repos/gems/src/app/text_area/new_file.h
@@ -0,0 +1,147 @@
+/*
+ * \brief Utility for writing data to a file via the Genode VFS library
+ * \author Norman Feske
+ * \date 2020-01-25
+ */
+
+/*
+ * 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.
+ */
+
+#ifndef _NEW_FILE_H_
+#define _NEW_FILE_H_
+
+#include
+
+namespace Genode { class New_file; }
+
+class Genode::New_file : Noncopyable
+{
+ public:
+
+ struct Create_failed : Exception { };
+
+ private:
+
+ Entrypoint &_ep;
+ Allocator &_alloc;
+ Vfs::File_system &_fs;
+ Vfs::Vfs_handle &_handle;
+
+ Vfs::Vfs_handle &_init_handle(Directory::Path const &path)
+ {
+ unsigned mode = Vfs::Directory_service::OPEN_MODE_WRONLY;
+
+ Vfs::Directory_service::Stat stat { };
+ if (_fs.stat(path.string(), stat) != Vfs::Directory_service::STAT_OK)
+ mode |= Vfs::Directory_service::OPEN_MODE_CREATE;
+
+ Vfs::Vfs_handle *handle_ptr = nullptr;
+ Vfs::Directory_service::Open_result const res =
+ _fs.open(path.string(), mode, &handle_ptr, _alloc);
+
+ if (res != Vfs::Directory_service::OPEN_OK || (handle_ptr == nullptr)) {
+ error("failed to create file '", path, "'");
+ throw Create_failed();
+ }
+
+ handle_ptr->fs().ftruncate(handle_ptr, 0);
+
+ return *handle_ptr;
+ }
+
+ public:
+
+ /**
+ * Constructor
+ *
+ * \throw Open_failed
+ */
+ New_file(Vfs::Env &env, Directory::Path const &path)
+ :
+ _ep(env.env().ep()), _alloc(env.alloc()), _fs(env.root_dir()),
+ _handle(_init_handle(path))
+ { }
+
+ ~New_file()
+ {
+ while (_handle.fs().queue_sync(&_handle) == false)
+ _ep.wait_and_dispatch_one_io_signal();
+
+ for (bool sync_done = false; !sync_done; ) {
+
+ switch (_handle.fs().complete_sync(&_handle)) {
+
+ case Vfs::File_io_service::SYNC_QUEUED:
+ break;
+
+ case Vfs::File_io_service::SYNC_ERR_INVALID:
+ warning("could not complete file sync operation");
+ sync_done = true;
+ break;
+
+ case Vfs::File_io_service::SYNC_OK:
+ sync_done = true;
+ break;
+ }
+
+ if (!sync_done)
+ _ep.wait_and_dispatch_one_io_signal();
+ }
+ _handle.ds().close(&_handle);
+ }
+
+ enum class Append_result { OK, WRITE_ERROR };
+
+ Append_result append(char const *src, size_t size)
+ {
+ bool write_error = false;
+
+ size_t remaining_bytes = size;
+
+ while (remaining_bytes > 0 && !write_error) {
+
+ bool stalled = false;
+
+ try {
+ Vfs::file_size out_count = 0;
+
+ using Write_result = Vfs::File_io_service::Write_result;
+
+ switch (_handle.fs().write(&_handle, src, remaining_bytes,
+ out_count)) {
+
+ case Write_result::WRITE_ERR_AGAIN:
+ case Write_result::WRITE_ERR_WOULD_BLOCK:
+ stalled = true;
+ break;
+
+ case Write_result::WRITE_ERR_INVALID:
+ case Write_result::WRITE_ERR_IO:
+ case Write_result::WRITE_ERR_INTERRUPT:
+ write_error = true;
+ break;
+
+ case Write_result::WRITE_OK:
+ out_count = min(remaining_bytes, out_count);
+ remaining_bytes -= out_count;
+ src += out_count;
+ _handle.advance_seek(out_count);
+ break;
+ };
+ }
+ catch (Vfs::File_io_service::Insufficient_buffer) {
+ stalled = true; }
+
+ if (stalled)
+ _ep.wait_and_dispatch_one_io_signal();
+ }
+ return write_error ? Append_result::WRITE_ERROR
+ : Append_result::OK;
+ }
+};
+
+#endif /* _NEW_FILE_H_ */
diff --git a/repos/gems/src/app/text_area/nitpicker.h b/repos/gems/src/app/text_area/nitpicker.h
new file mode 100644
index 000000000..6eb447e3b
--- /dev/null
+++ b/repos/gems/src/app/text_area/nitpicker.h
@@ -0,0 +1,122 @@
+/*
+ * \brief Nitpicker wrapper for monitoring the user input of GUI components
+ * \author Norman Feske
+ * \date 2020-01-12
+ */
+
+/*
+ * 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.
+ */
+
+#ifndef _NITPICKER_H_
+#define _NITPICKER_H_
+
+/* Genode includes */
+#include
+#include
+#include
+
+/* local includes */
+#include
+
+namespace Nitpicker {
+
+ using namespace Genode;
+
+ struct Session_component;
+}
+
+
+struct Nitpicker::Session_component : Session_object
+{
+ Env &_env;
+
+ Input_event_handler &_event_handler;
+
+ Nitpicker::Connection _connection;
+
+ Input::Session_component _input_component { _env, _env.ram() };
+
+ Signal_handler _input_handler {
+ _env.ep(), *this, &Session_component::_handle_input };
+
+ void _handle_input()
+ {
+ _connection.input()->for_each_event([&] (Input::Event ev) {
+
+ /* handle event locally within the sculpt manager */
+ _event_handler.handle_input_event(ev);
+
+ _input_component.submit(ev);
+ });
+ }
+
+ template
+ Session_component(Env &env, Input_event_handler &event_handler, ARGS &&... args)
+ :
+ Session_object(args...),
+ _env(env), _event_handler(event_handler),
+ _connection(env, _label.string())
+ {
+ _connection.input()->sigh(_input_handler);
+ _env.ep().manage(_input_component);
+ _input_component.event_queue().enabled(true);
+ }
+
+ ~Session_component() { _env.ep().dissolve(_input_component); }
+
+ void upgrade(Session::Resources const &resources)
+ {
+ _connection.upgrade(resources);
+ }
+
+ Framebuffer::Session_capability framebuffer_session() override {
+ return _connection.framebuffer_session(); }
+
+ Input::Session_capability input_session() override {
+ return _input_component.cap(); }
+
+ View_handle create_view(View_handle parent) override {
+ return _connection.create_view(parent); }
+
+ void destroy_view(View_handle view) override {
+ _connection.destroy_view(view); }
+
+ View_handle view_handle(View_capability view_cap, View_handle handle) override {
+ return _connection.view_handle(view_cap, handle); }
+
+ View_capability view_capability(View_handle view) override {
+ return _connection.view_capability(view); }
+
+ void release_view_handle(View_handle view) override {
+ _connection.release_view_handle(view); }
+
+ Dataspace_capability command_dataspace() override {
+ return _connection.command_dataspace(); }
+
+ void execute() override {
+ _connection.execute(); }
+
+ Framebuffer::Mode mode() override {
+ return _connection.mode(); }
+
+ void mode_sigh(Signal_context_capability sigh) override {
+ _connection.mode_sigh(sigh); }
+
+ void buffer(Framebuffer::Mode mode, bool use_alpha) override
+ {
+ /*
+ * Do not call 'Connection::buffer' to avoid paying session quota
+ * from our own budget.
+ */
+ _connection.Client::buffer(mode, use_alpha);
+ }
+
+ void focus(Capability session) override {
+ _connection.focus(session); }
+};
+
+#endif /* _NITPICKER_H_ */
diff --git a/repos/gems/src/app/text_area/report.h b/repos/gems/src/app/text_area/report.h
new file mode 100644
index 000000000..f9e402c1c
--- /dev/null
+++ b/repos/gems/src/app/text_area/report.h
@@ -0,0 +1,89 @@
+/*
+ * \brief Report session provided to the sandbox
+ * \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.
+ */
+
+#ifndef _REPORT_H_
+#define _REPORT_H_
+
+/* Genode includes */
+#include
+#include
+
+namespace Report {
+
+ using namespace Genode;
+
+ struct Session_component;
+}
+
+
+class Report::Session_component : public Session_object
+{
+ public:
+
+ struct Handler_base : Interface, Genode::Noncopyable
+ {
+ virtual void handle_report(char const *, size_t) = 0;
+ };
+
+ template
+ struct Xml_handler : Handler_base
+ {
+ T &_obj;
+ void (T::*_member) (Xml_node const &);
+
+ Xml_handler(T &obj, void (T::*member)(Xml_node const &))
+ : _obj(obj), _member(member) { }
+
+ void handle_report(char const *start, size_t length) override
+ {
+ (_obj.*_member)(Xml_node(start, length));
+ }
+ };
+
+ private:
+
+ Attached_ram_dataspace _ds;
+
+ Handler_base &_handler;
+
+
+ /*******************************
+ ** Report::Session interface **
+ *******************************/
+
+ Dataspace_capability dataspace() override { return _ds.cap(); }
+
+ void submit(size_t length) override
+ {
+ _handler.handle_report(_ds.local_addr(),
+ min(_ds.size(), length));
+ }
+
+ void response_sigh(Signal_context_capability) override { }
+
+ size_t obtain_response() override { return 0; }
+
+ public:
+
+ template
+ Session_component(Env &env, Handler_base &handler,
+ Entrypoint &ep, Resources const &resources,
+ ARGS &&... args)
+ :
+ Session_object(ep, resources, args...),
+ _ds(env.ram(), env.rm(), resources.ram_quota.value),
+ _handler(handler)
+ { }
+};
+
+#endif /* _REPORT_H_ */
diff --git a/repos/gems/src/app/text_area/target.mk b/repos/gems/src/app/text_area/target.mk
new file mode 100644
index 000000000..b5643b156
--- /dev/null
+++ b/repos/gems/src/app/text_area/target.mk
@@ -0,0 +1,4 @@
+TARGET = text_area
+SRC_CC = main.cc dialog.cc
+LIBS += base sandbox vfs
+INC_DIR += $(PRG_DIR)
diff --git a/repos/gems/src/app/text_area/types.h b/repos/gems/src/app/text_area/types.h
new file mode 100644
index 000000000..303a626e2
--- /dev/null
+++ b/repos/gems/src/app/text_area/types.h
@@ -0,0 +1,21 @@
+/*
+ * \brief Common types
+ * \author Norman Feske
+ * \date 2020-01-14
+ */
+
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#ifndef _TYPES_H_
+#define _TYPES_H_
+
+namespace Genode { }
+
+namespace Text_area { using namespace Genode; }
+
+#endif /* _TYPES_H_ */