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
This commit is contained in:
Norman Feske 2023-07-19 17:08:01 +02:00 committed by Christian Helmuth
parent 9d5af71c3d
commit 0c40d52010
3 changed files with 554 additions and 7 deletions

View File

@ -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 <dialog/distant_runtime.h>
#include <xml.h>
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<Event::Seq_number> &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<Cpu_session> (xml);
gen_parent_route<Pd_session> (xml);
gen_parent_route<Log_session> (xml);
gen_parent_route<Timer::Session> (xml);
gen_service_node<Gui::Session>(xml, [&] {
xml.node("parent", [&] {
xml.attribute("label", Label("leitzentrale -> ", _start_name)); }); });
gen_service_node<Rom_session>(xml, [&] {
xml.attribute("label", "dialog");
xml.node("parent", [&] {
xml.attribute("label", Label("leitzentrale -> ", _start_name, " -> dialog"));
});
});
gen_service_node<Report::Session>(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"); }); });
});
});
}

View File

@ -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 <os/reporter.h>
#include <base/attached_rom_dataspace.h>
#include <util/dictionary.h>
#include <util/color.h>
#include <dialog/types.h>
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 <typename T> class Event_handler;
private:
Env &_env;
using Views = Dictionary<View, Top_level_dialog::Name>;
Event::Seq_number _global_seq_number { 1 };
Views _views { };
/* sequence numbers to correlate hover info with click/clack events */
Constructible<Event::Seq_number> _click_seq_number { };
Constructible<Event::Seq_number> _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<View, Top_level_dialog::Name>;
friend class Avl_node<View>;
friend class Avl_tree<View>;
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<View> _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 <typename FN>
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("<empty/>"));
}
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<View> _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("<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_ */

View File

@ -18,6 +18,37 @@
#include <gui_session/connection.h>
#include <base/heap.h>
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<Gui::Session>
{
Env &_env;
@ -33,20 +64,23 @@ struct Gui::Session_component : Rpc_object<Gui::Session>
Signal_handler<Session_component> _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);
}