From 0c40d52010526684b5c8fb54180b1e16f4ef29d6 Mon Sep 17 00:00:00 2001 From: Norman Feske Date: Wed, 19 Jul 2023 17:08:01 +0200 Subject: [PATCH] sculpt: add Dialog::Distant_runtime The so-called 'Distant_runtime' implements GUI dialogs via menu_view components hosted at a distant init instance as opposed to child components (as implemented by the 'Sandboxed_runtime'). This is particular the case in Sculpt OS where the sculpt manager is not the parent of the menu_view instances. Issue #5008 --- .../sculpt_manager/dialog/distant_runtime.cc | 254 +++++++++++++++++ .../sculpt_manager/dialog/distant_runtime.h | 259 ++++++++++++++++++ repos/gems/src/app/sculpt_manager/gui.cc | 48 +++- 3 files changed, 554 insertions(+), 7 deletions(-) create mode 100644 repos/gems/src/app/sculpt_manager/dialog/distant_runtime.cc create mode 100644 repos/gems/src/app/sculpt_manager/dialog/distant_runtime.h diff --git a/repos/gems/src/app/sculpt_manager/dialog/distant_runtime.cc b/repos/gems/src/app/sculpt_manager/dialog/distant_runtime.cc new file mode 100644 index 0000000000..12a6e0762d --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/dialog/distant_runtime.cc @@ -0,0 +1,254 @@ +/* + * \brief Runtime for hosting GUI dialogs in child components + * \author Norman Feske + * \date 2023-03-24 + */ + +/* + * Copyright (C) 2023 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. + */ + +#include +#include + +using namespace Sculpt; +using namespace Dialog; + + +static bool click(Input::Event const &event) +{ + bool result = false; + + if (event.key_press(Input::BTN_LEFT)) + result = true; + + event.handle_touch([&] (Input::Touch_id id, float, float) { + if (id.value == 0) + result = true; }); + + return result; +} + + +static bool clack(Input::Event const &event) +{ + bool result = false; + + if (event.key_release(Input::BTN_LEFT)) + result = true; + + event.handle_touch_release([&] (Input::Touch_id id) { + if (id.value == 0) + result = true; }); + + return result; +} + + +bool Distant_runtime::apply_runtime_state(Xml_node const &state) +{ + using Name = Top_level_dialog::Name; + + /* the dialog name is the start name with the "_view" suffix removed */ + auto with_dialog_name = [] (Start_name const &name, auto const &fn) + { + if (name.length() > 6) { + size_t const dialog_name_len = name.length() - 6; + char const * const view_suffix_ptr = name.string() + dialog_name_len; + if (strcmp(view_suffix_ptr, "_view") == 0) + fn(Name(Cstring(name.string(), dialog_name_len))); + } + }; + + bool reconfiguration_needed = false; + state.for_each_sub_node("child", [&] (Xml_node const &child) { + Start_name const start_name = child.attribute_value("name", Start_name()); + with_dialog_name(start_name, [&] (Name const &name) { + _views.with_element(name, + [&] (View &view) { + if (view._apply_child_state_report(child)) + reconfiguration_needed = true; }, + [&] /* no view named after this child */ { }); + }); + }); + + return reconfiguration_needed; +} + + +void Distant_runtime::gen_start_nodes(Xml_generator &xml) const +{ + _views.for_each([&] (View const &view) { + view._gen_start_node(xml); }); +} + + +void Distant_runtime::route_input_event(Event::Seq_number seq_number, Input::Event const &event) +{ + _global_seq_number = seq_number; + + if (event.absolute_motion()) _hover_observable_without_click = true; + if (event.touch()) _hover_observable_without_click = false; + + if (click(event) && !_click_seq_number.constructed()) { + _click_seq_number.construct(_global_seq_number); + _click_delivered = false; + } + + if (clack(event)) + _clack_seq_number.construct(_global_seq_number); + + _try_handle_click_and_clack(); +} + + +void Distant_runtime::_try_handle_click_and_clack() +{ + auto with_hovered_view = [&] (Event::Seq_number seq_number, auto const &fn) + { + /* find name of dialog hovered with matching 'seq_number' */ + Top_level_dialog::Name name { }; + _views.for_each([&] (View const &view) { + if (seq_number == view._hover_seq_number) + name = view._dialog.name; }); + + /* apply 'fn' with (non-const) view as argument */ + if (name.valid()) + _views.with_element(name, + [&] (View &view) { fn(view); }, + [&] { }); + }; + + Constructible &click = _click_seq_number, + &clack = _clack_seq_number; + + if (!_click_delivered && click.constructed()) { + with_hovered_view(*click, [&] (View &view) { + view._with_dialog_hover([&] (Xml_node const &hover) { + Clicked_at at(*click, hover); + view._dialog.click(at); + _click_delivered = true; + view.refresh(); + }); + }); + } + + if (click.constructed() && clack.constructed()) { + with_hovered_view(*clack, [&] (View &view) { + view._with_dialog_hover([&] (Xml_node const &hover) { + + /* + * Deliver stale click if the hover report for the clack + * overwrote the intermediate hover report for the click. + */ + if (!_click_delivered) { + Clicked_at at(*click, hover); + view._dialog.click(at); + _click_delivered = true; + } + + /* use click seq number for to associate clack with click */ + Clacked_at at(*click, hover); + view._dialog.clack(at); + view.refresh(); + }); + + click.destruct(); + clack.destruct(); + }); + } +} + + +void Distant_runtime::View::_gen_start_node(Xml_generator &xml) const +{ + xml.node("start", [&] { + + xml.attribute("name", _start_name); + xml.attribute("version", _version); + xml.attribute("caps", _caps.value); + + xml.node("resource", [&] { + xml.attribute("name", "RAM"); + Number_of_bytes const bytes(_ram.value); + xml.attribute("quantum", String<64>(bytes)); }); + + xml.node("binary", [&] { + xml.attribute("name", "menu_view"); }); + + xml.node("heartbeat", [&] { }); + + xml.node("config", [&] { + + if (min_width) xml.attribute("width", min_width); + if (min_height) xml.attribute("height", min_height); + if (_opaque) xml.attribute("opaque", "yes"); + + xml.attribute("background", String<20>(_background)); + + 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"); + }); + }); + }); + }); + + using Label = Session_label::String; + + xml.node("route", [&] { + gen_parent_rom_route(xml, "menu_view"); + gen_parent_rom_route(xml, "ld.lib.so"); + gen_parent_rom_route(xml, "vfs.lib.so"); + gen_parent_rom_route(xml, "libc.lib.so"); + gen_parent_rom_route(xml, "libm.lib.so"); + gen_parent_rom_route(xml, "libpng.lib.so"); + gen_parent_rom_route(xml, "zlib.lib.so"); + gen_parent_rom_route(xml, "menu_view_styles.tar"); + gen_parent_route (xml); + gen_parent_route (xml); + gen_parent_route (xml); + gen_parent_route (xml); + + gen_service_node(xml, [&] { + xml.node("parent", [&] { + xml.attribute("label", Label("leitzentrale -> ", _start_name)); }); }); + + gen_service_node(xml, [&] { + xml.attribute("label", "dialog"); + xml.node("parent", [&] { + xml.attribute("label", Label("leitzentrale -> ", _start_name, " -> dialog")); + }); + }); + + gen_service_node(xml, [&] { + xml.attribute("label", "hover"); + xml.node("parent", [&] { + xml.attribute("label", Label("leitzentrale -> ", _start_name, " -> hover")); + }); + }); + + gen_service_node<::File_system::Session>(xml, [&] { + xml.attribute("label", "fonts"); + xml.node("parent", [&] { + xml.attribute("label", "leitzentrale -> fonts"); }); }); + }); + }); +} diff --git a/repos/gems/src/app/sculpt_manager/dialog/distant_runtime.h b/repos/gems/src/app/sculpt_manager/dialog/distant_runtime.h new file mode 100644 index 0000000000..d49246d3fa --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/dialog/distant_runtime.h @@ -0,0 +1,259 @@ +/* + * \brief Runtime for hosting GUI dialogs in distant menu-view instances + * \author Norman Feske + * \date 2023-07-19 + */ + +/* + * Copyright (C) 2023 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__DISTANT_RUNTIME_H_ +#define _DIALOG__DISTANT_RUNTIME_H_ + +#include +#include +#include +#include +#include + +namespace Dialog { struct Distant_runtime; } + + +class Dialog::Distant_runtime : Noncopyable +{ + public: + + class View; + + struct Event_handler_base : Interface, Noncopyable + { + virtual void handle_event(Event const &event) = 0; + }; + + template class Event_handler; + + private: + + Env &_env; + + using Views = Dictionary; + + Event::Seq_number _global_seq_number { 1 }; + + Views _views { }; + + /* sequence numbers to correlate hover info with click/clack events */ + Constructible _click_seq_number { }; + Constructible _clack_seq_number { }; + + bool _click_delivered = false; /* used to deliver each click only once */ + + bool _dragged() const + { + return _click_seq_number.constructed() + && *_click_seq_number == _global_seq_number + && _click_delivered; + } + + /* true when using a pointer device, false when using touch */ + bool _hover_observable_without_click = false; + + void _try_handle_click_and_clack(); + + public: + + Distant_runtime(Env &env) : _env(env) { } + + /** + * Route input event to the 'Top_level_dialog' click/clack interfaces + */ + void route_input_event(Event::Seq_number, Input::Event const &); + + /** + * Respond to runtime-init state changes + * + * \return true if the runtime-init configuration needs to be updated + */ + bool apply_runtime_state(Xml_node const &); + + void gen_start_nodes(Xml_generator &) const; +}; + + +class Dialog::Distant_runtime::View : private Views::Element +{ + private: + + /* needed for privately inheriting 'Views::Element' */ + friend class Dictionary; + friend class Avl_node; + friend class Avl_tree; + + friend class Distant_runtime; + + using Start_name = Session_label::String; + + Env &_env; + Distant_runtime &_runtime; + Top_level_dialog &_dialog; + + Start_name const _start_name { _dialog.name, "_view" }; + Ram_quota const _initial_ram { 4*1024*1024 }; + Cap_quota const _initial_caps { 200 }; + + bool const _opaque; + Color const _background; + + Ram_quota _ram = _initial_ram; + Cap_quota _caps = _initial_caps; + + unsigned _version = 0; + + Expanding_reporter _dialog_reporter { + _env, "dialog", { _dialog.name, "_dialog" } }; + + Attached_rom_dataspace _hover_rom { + _env, Session_label::String(_dialog.name, "_view_hover").string() }; + + Signal_handler _hover_handler { _env.ep(), *this, &View::_handle_hover }; + + bool _dialog_hovered = false; /* used to cut hover feedback loop */ + + Event::Seq_number _hover_seq_number { }; + + template + void _with_dialog_hover(FN const &fn) const + { + bool done = false; + + _hover_rom.xml().with_optional_sub_node("dialog", [&] (Xml_node const &dialog) { + fn(dialog); + done = true; }); + + if (!done) + fn(Xml_node("")); + } + + void _handle_hover() + { + _hover_rom.update(); + + Xml_node const hover = _hover_rom.xml(); + + bool const orig_dialog_hovered = _dialog_hovered; + + _hover_seq_number = { hover.attribute_value("seq_number", 0U) }; + _dialog_hovered = (hover.num_sub_nodes() > 0); + + if (_runtime._dragged()) { + _with_dialog_hover([&] (Xml_node const &hover) { + Dragged_at at(*_runtime._click_seq_number, hover); + _dialog.drag(at); + }); + } + + _runtime._try_handle_click_and_clack(); + + if (orig_dialog_hovered != _dialog_hovered || _dialog_hovered) + _generate_dialog(); + } + + Signal_handler _refresh_handler { _env.ep(), *this, &View::_generate_dialog }; + + void _generate_dialog() + { + _dialog_reporter.generate([&] (Xml_generator &xml) { + _with_dialog_hover([&] (Xml_node const &hover) { + + Event::Dragged const dragged { _runtime._dragged() }; + + bool const supply_hover = _runtime._hover_observable_without_click + || dragged.value; + + static Xml_node omitted_hover(""); + + At const at { _runtime._global_seq_number, + supply_hover ? hover : omitted_hover }; + + Scope<> top_level_scope(xml, at, dragged, { _dialog.name }); + _dialog.view(top_level_scope); + }); + }); + } + + /** + * 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 sandbox state report + * \return true if runtime must be reconfigured so that the changes + * can take effect + */ + bool _apply_child_state_report(Xml_node const &child) + { + bool result = false; + + if (child.attribute_value("name", Start_name()) != _start_name) + return false; + + if (child.has_sub_node("ram") && child.sub_node("ram").has_attribute("requested")) { + _ram.value *= 2; + result = true; + } + + if (child.has_sub_node("caps") && child.sub_node("caps").has_attribute("requested")) { + _caps.value += 100; + result = true; + } + + if (child.attribute_value("skipped_heartbeats", 0U) > 2) { + _version++; + _ram = _initial_ram; + _caps = _initial_caps; + result = true; + } + + return result; + } + + void _gen_start_node(Xml_generator &) const; + + public: + + unsigned min_width = 0, min_height = 0; + + struct Attr + { + bool opaque; + Color background; + Ram_quota initial_ram; + }; + + View(Distant_runtime &runtime, Top_level_dialog &dialog, Attr attr) + : + Views::Element(runtime._views, dialog.name), + _env(runtime._env), _runtime(runtime), _dialog(dialog), + _initial_ram(attr.initial_ram), _opaque(attr.opaque), + _background(attr.background) + { + _hover_rom.sigh(_hover_handler); + _refresh_handler.local_submit(); + } + + View(Distant_runtime &runtime, Top_level_dialog &dialog) + : + View(runtime, dialog, Attr { .opaque = false, + .background = { }, + .initial_ram = { 4*1024*1024 } }) + { } + + void refresh() { _refresh_handler.local_submit(); } +}; + +#endif /* _DIALOG__DISTANT_RUNTIME_H_ */ diff --git a/repos/gems/src/app/sculpt_manager/gui.cc b/repos/gems/src/app/sculpt_manager/gui.cc index 221d623c17..ec7253aa5e 100644 --- a/repos/gems/src/app/sculpt_manager/gui.cc +++ b/repos/gems/src/app/sculpt_manager/gui.cc @@ -18,6 +18,37 @@ #include #include + +static bool click(Input::Event const &event) +{ + bool result = false; + + if (event.key_press(Input::BTN_LEFT)) + result = true; + + event.handle_touch([&] (Input::Touch_id id, float, float) { + if (id.value == 0) + result = true; }); + + return result; +} + + +static bool clack(Input::Event const &event) +{ + bool result = false; + + if (event.key_release(Input::BTN_LEFT)) + result = true; + + event.handle_touch_release([&] (Input::Touch_id id) { + if (id.value == 0) + result = true; }); + + return result; +} + + struct Gui::Session_component : Rpc_object { Env &_env; @@ -33,20 +64,23 @@ struct Gui::Session_component : Rpc_object Signal_handler _input_handler { _env.ep(), *this, &Session_component::_handle_input }; + bool _clicked = false; + void _handle_input() { _connection.input()->for_each_event([&] (Input::Event ev) { /* - * Augment input stream with sequence numbers to correlate - * clicks with hover reports. + * Assign new event sequence number, pass seq event to menu view + * to ensure freshness of hover information. */ - bool const advance_seq_number = ev.key_press(Input::BTN_LEFT) - || ev.key_release(Input::BTN_LEFT) - || ev.touch() || ev.touch_release(); - if (advance_seq_number) { - _global_input_seq_number.value++; + bool const orig_clicked = _clicked; + if (click(ev)) _clicked = true; + if (clack(ev)) _clicked = false; + + if (orig_clicked != _clicked) { + _global_input_seq_number.value++; _input_component.submit(_global_input_seq_number); }