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_ */