Dialog API

The new API at gems/include/dialog/ aids the creation of simple GUI
applications based on the menu-view widget renderer. Its use is
illustrated by the simple test application at src/test/dialog/
that is accompanied with the dialog.run script.

Issue #5008
This commit is contained in:
Norman Feske 2023-03-27 00:28:49 +02:00 committed by Christian Helmuth
parent 6895175764
commit 4fdc999087
10 changed files with 1873 additions and 0 deletions

View File

@ -0,0 +1,110 @@
/*
* \brief Wrapper around 'Sandboxed_runtime' for simple applications
* \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.
*/
#ifndef _INCLUDE__DIALOG__RUNTIME_H_
#define _INCLUDE__DIALOG__RUNTIME_H_
#include <dialog/sandboxed_runtime.h>
#include <os/buffered_xml.h>
namespace Dialog { struct Runtime; }
class Dialog::Runtime : private Sandbox::State_handler
{
private:
Env &_env;
Allocator &_alloc;
Sandbox _sandbox { _env, *this };
Sandboxed_runtime _runtime { _env, _alloc, _sandbox };
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("Gui");
service_node("Timer");
service_node("Report");
service_node("File_system");
});
_runtime.gen_start_nodes(xml);
}
void _update_sandbox_config()
{
Buffered_xml const config { _alloc, "config", [&] (Xml_generator &xml) {
_generate_sandbox_config(xml); } };
config.with_xml_node([&] (Xml_node const &config) {
_sandbox.apply_config(config); });
}
/**
* Sandbox::State_handler
*/
void handle_sandbox_state() override
{
/* obtain current sandbox state */
Buffered_xml state(_alloc, "state", [&] (Xml_generator &xml) {
_sandbox.generate_state_report(xml); });
bool reconfiguration_needed = false;
state.with_xml_node([&] (Xml_node state) {
if (_runtime.apply_sandbox_state(state))
reconfiguration_needed = true; });
if (reconfiguration_needed)
_update_sandbox_config();
}
public:
Runtime(Env &env, Allocator &alloc) : _env(env), _alloc(alloc) { }
struct View : Sandboxed_runtime::View
{
View(Runtime &runtime, Top_level_dialog &dialog)
:
Sandboxed_runtime::View(runtime._runtime, dialog)
{
runtime._update_sandbox_config();
}
};
template <typename T>
struct Event_handler : Sandboxed_runtime::Event_handler<T>
{
Event_handler(Runtime &runtime, T &obj, void (T::*member)(Event const &))
: Sandboxed_runtime::Event_handler<T>(runtime._runtime, obj, member) { }
};
};
#endif /* _INCLUDE__DIALOG__RUNTIME_H_ */

View File

@ -0,0 +1,378 @@
/*
* \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.
*/
#ifndef _INCLUDE__DIALOG__SANDBOXED_RUNTIME_H_
#define _INCLUDE__DIALOG__SANDBOXED_RUNTIME_H_
#include <util/dictionary.h>
#include <os/dynamic_rom_session.h>
#include <base/session_object.h>
#include <report_session/report_session.h>
#include <sandbox/sandbox.h>
#include <dialog/types.h>
namespace Dialog { struct Sandboxed_runtime; }
class Dialog::Sandboxed_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;
Allocator &_alloc;
Sandbox &_sandbox;
using Views = Dictionary<View, Top_level_dialog::Name>;
Event::Seq_number _global_seq_number { 1 };
Views _views { };
struct Gui_session;
struct Report_session;
using Gui_service = Sandbox::Local_service<Gui_session>;
using Rom_service = Sandbox::Local_service<Dynamic_rom_session>;
using Report_service = Sandbox::Local_service<Report_session>;
void _handle_gui_service();
void _handle_rom_service();
void _handle_report_service();
struct Service_handler : Sandbox::Local_service_base::Wakeup
{
Sandboxed_runtime &_runtime;
using Member = void (Sandboxed_runtime::*) ();
Member _member;
void wakeup_local_service() override
{
(_runtime.*_member)();
}
Service_handler(Sandboxed_runtime &runtime, Member member)
: _runtime(runtime), _member(member) { }
};
Service_handler _gui_handler { *this, &Sandboxed_runtime::_handle_gui_service };
Service_handler _rom_handler { *this, &Sandboxed_runtime::_handle_rom_service };
Service_handler _report_handler { *this, &Sandboxed_runtime::_handle_report_service };
Gui_service _gui_service;
Rom_service _rom_service;
Report_service _report_service;
public:
Sandboxed_runtime(Env &, Allocator &, Sandbox &);
/**
* Respond to sandbox state changes
*
* \return true if the sandbox configuration needs to be updated
*/
bool apply_sandbox_state(Xml_node const &);
void gen_start_nodes(Xml_generator &) const;
};
class Dialog::Sandboxed_runtime::Report_session : public Session_object<Report::Session>
{
public:
struct Handler : Interface, Genode::Noncopyable
{
virtual void handle_report() = 0;
};
private:
Attached_ram_dataspace _client_ds;
Attached_ram_dataspace _local_ds;
Constructible<Xml_node> _xml { }; /* points inside _local_ds */
Handler &_handler;
/*******************************
** Report::Session interface **
*******************************/
Dataspace_capability dataspace() override { return _client_ds.cap(); }
void submit(size_t length) override
{
size_t const num_bytes = min(_client_ds.size(), length);
memcpy(_local_ds.local_addr<char>(), _client_ds.local_addr<char>(),
num_bytes);
_xml.destruct();
try { _xml.construct(_local_ds.local_addr<char>(), num_bytes); }
catch (...) { }
_handler.handle_report();
}
void response_sigh(Signal_context_capability) override { }
size_t obtain_response() override { return 0; }
public:
template <typename... ARGS>
Report_session(Env &env, Handler &handler,
Entrypoint &ep, Resources const &resources,
ARGS &&... args)
:
Session_object(ep, resources, args...),
_client_ds(env.ram(), env.rm(), resources.ram_quota.value/2),
_local_ds (env.ram(), env.rm(), resources.ram_quota.value/2),
_handler(handler)
{ }
template <typename FN>
void with_xml(FN const &fn) const
{
if (_xml.constructed())
fn(*_xml);
else
fn(Xml_node("<empty/>"));
}
};
class Dialog::Sandboxed_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>;
public:
Env &_env;
Allocator &_alloc;
Event::Seq_number &_global_seq_number;
Top_level_dialog &_dialog;
bool _dialog_hovered = false; /* used to cut hover feedback loop */
/* sequence numbers to correlate hover info with click/clack events */
Event::Seq_number _hover_seq_number { };
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;
}
bool _hover_observable_without_click = false;
struct Rom_producer : Dynamic_rom_session::Xml_producer
{
View const &_view;
Rom_producer(View const &view)
:
Dynamic_rom_session::Xml_producer("dialog"),
_view(view)
{ }
void produce_xml(Xml_generator &xml) override
{
_view._with_dialog_hover([&] (Xml_node const &hover) {
Event::Dragged const dragged { _view._dragged() };
bool const supply_hover = _view._hover_observable_without_click
|| dragged.value;
static Xml_node omitted_hover("<hover/>");
At const at { _view._global_seq_number,
supply_hover ? hover : omitted_hover };
Scope<> top_level_scope(xml, at, dragged, { _view._dialog.name });
_view._dialog.view(top_level_scope);
});
}
} _dialog_producer { *this };
Dynamic_rom_session _dialog_rom_session {
_env.ep(), _env.ram(), _env.rm(), _dialog_producer };
template <typename T>
struct Hover_handler : Report_session::Handler
{
T &_obj;
void (T::*_member) ();
Hover_handler(T &obj, void (T::*member)())
: _obj(obj), _member(member) { }
void handle_report() override
{
(_obj.*_member)();
}
};
Constructible<Report_session> _hover_report_session { };
template <typename FN>
void _with_dialog_hover(FN const &fn) const
{
bool done = false;
if (_hover_report_session.constructed())
_hover_report_session->with_xml([&] (Xml_node const &hover) {
hover.with_optional_sub_node("dialog", [&] (Xml_node const &dialog) {
fn(dialog);
done = true; }); });
if (!done)
fn(Xml_node("<empty/>"));
}
void _handle_input_event(Input::Event const &);
void _handle_hover();
Hover_handler<View> _hover_handler { *this, &View::_handle_hover };
void _try_handle_click_and_clack();
struct Menu_view_state
{
using Start_name = String<128>;
Start_name const _name;
Ram_quota const _initial_ram;
Cap_quota const _initial_caps;
Ram_quota _ram = _initial_ram;
Cap_quota _caps = _initial_caps;
unsigned _version = 0;
void trigger_restart()
{
_version++;
_ram = _initial_ram;
_caps = _initial_caps;
}
/**
* 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()) != _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;
}
return result;
}
Menu_view_state(Top_level_dialog::Name const &name, Ram_quota ram, Cap_quota caps)
:
_name(name), _initial_ram(ram), _initial_caps(caps)
{ }
void gen_start_node(Xml_generator &) const;
} _menu_view_state;
Registry<Gui_session> _gui_sessions { };
public:
View(Sandboxed_runtime &runtime, Top_level_dialog &dialog)
:
Views::Element(runtime._views, dialog.name),
_env(runtime._env), _alloc(runtime._alloc),
_global_seq_number(runtime._global_seq_number),
_dialog(dialog),
_menu_view_state(dialog.name, Ram_quota { 4*1024*1024 }, Cap_quota { 200 })
{ }
~View();
void refresh() { _dialog_rom_session.trigger_update(); }
};
template <typename T>
class Dialog::Sandboxed_runtime::Event_handler : Event_handler_base
{
private:
T &_obj;
void (T::*_member) (Event const &);
void handle_event(Event const &event) override { (_obj.*_member)(event); }
public:
Event_handler(Sandboxed_runtime &runtime, T &obj, void (T::*member)(Event const &))
: _obj(obj), _member(member)
{
/* register event handler at runtime */
(void)runtime;
}
};
#endif /* _INCLUDE__DIALOG__SANDBOXED_RUNTIME_H_ */

View File

@ -0,0 +1,171 @@
/*
* \brief Sub-scope types
* \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.
*/
#ifndef _INCLUDE__DIALOG__SUB_SCOPES_H_
#define _INCLUDE__DIALOG__SUB_SCOPES_H_
#include <dialog/types.h>
namespace Dialog {
template <typename AT, typename FN>
static void with_narrowed_xml(AT const &, char const *xml_type, FN const &fn);
struct Vbox;
struct Hbox;
struct Frame;
struct Float;
struct Button;
struct Label;
struct Min_ex;
struct Depgraph;
}
template <typename AT, typename FN>
static inline void Dialog::with_narrowed_xml(AT const &at, char const *xml_type, FN const &fn)
{
at._location.with_optional_sub_node(xml_type, [&] (Xml_node const &node) {
AT const narrowed_at { at.seq_number, node };
fn(narrowed_at);
});
}
struct Dialog::Vbox : Sub_scope
{
template <typename SCOPE, typename FN>
static void view_sub_scope(SCOPE &s, FN const &fn)
{
s.node("vbox", [&] { fn(s); });
}
template <typename AT, typename FN>
static void with_narrowed_at(AT const &at, FN const &fn) {
with_narrowed_xml(at, "vbox", fn); }
};
struct Dialog::Hbox : Sub_scope
{
template <typename SCOPE, typename FN>
static void view_sub_scope(SCOPE &s, FN const &fn)
{
s.node("hbox", [&] { fn(s); });
}
template <typename SCOPE>
static void view_sub_scope(SCOPE &s) { s.node("hbox", [&] { }); }
template <typename AT, typename FN>
static void with_narrowed_at(AT const &at, FN const &fn) {
with_narrowed_xml(at, "hbox", fn); }
};
struct Dialog::Frame : Sub_scope
{
template <typename SCOPE, typename FN>
static void view_sub_scope(SCOPE &s, FN const &fn)
{
s.node("frame", [&] { fn(s); });
}
template <typename AT, typename FN>
static void with_narrowed_at(AT const &at, FN const &fn) {
with_narrowed_xml(at, "frame", fn); }
};
struct Dialog::Float : Sub_scope
{
template <typename SCOPE, typename FN>
static void view_sub_scope(SCOPE &s, FN const &fn)
{
s.node("float", [&] { fn(s); });
}
template <typename AT, typename FN>
static void with_narrowed_at(AT const &at, FN const &fn) {
with_narrowed_xml(at, "float", fn); }
};
struct Dialog::Button : Sub_scope
{
template <typename SCOPE, typename FN>
static void view_sub_scope(SCOPE &s, FN const &fn)
{
s.node("button", [&] {
fn(s); });
}
template <typename AT, typename FN>
static void with_narrowed_at(AT const &at, FN const &fn) {
with_narrowed_xml(at, "button", fn); }
};
struct Dialog::Label : Sub_scope
{
template <typename SCOPE, typename TEXT>
static void view_sub_scope(SCOPE &s, TEXT const &text)
{
s.node("label", [&] {
s.attribute("text", text); });
}
template <typename SCOPE, typename TEXT, typename FN>
static void view_sub_scope(SCOPE &s, TEXT const &text, FN const &fn)
{
s.node("label", [&] {
s.attribute("text", text);
fn(s);
});
}
template <typename AT, typename FN>
static void with_narrowed_at(AT const &at, FN const &fn) {
with_narrowed_xml(at, "label", fn); }
};
struct Dialog::Min_ex : Sub_scope
{
template <typename SCOPE>
static void view_sub_scope(SCOPE &s, unsigned min_ex)
{
s.node("label", [&] {
s.attribute("min_ex", min_ex); });
}
template <typename AT, typename FN>
static void with_narrowed_at(AT const &at, FN const &fn) {
with_narrowed_xml(at, "label", fn); }
};
struct Dialog::Depgraph : Sub_scope
{
template <typename SCOPE, typename FN>
static void view_sub_scope(SCOPE &s, FN const &fn)
{
s.node("depgraph", [&] { fn(s); });
}
template <typename AT, typename FN>
static void with_narrowed_at(AT const &at, FN const &fn) {
with_narrowed_xml(at, "depgraph", fn); }
};
#endif /* _INCLUDE__DIALOG__SUB_SCOPES_H_ */

View File

@ -0,0 +1,410 @@
/*
* \brief Fundamental types for implementing GUI dialogs
* \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.
*/
#ifndef _INCLUDE__DIALOG__TYPES_H_
#define _INCLUDE__DIALOG__TYPES_H_
/* Genode includes */
#include <util/xml_node.h>
#include <util/xml_generator.h>
#include <base/log.h>
#include <input/event.h>
#include <input/keycodes.h>
namespace Dialog {
using namespace Genode;
struct Id;
struct Event;
struct At;
struct Clicked_at;
struct Clacked_at;
struct Dragged_at;
static inline Clicked_at const &clicked_at(At const &at);
template <typename...> struct Scope;
struct Sub_scope;
struct Top_level_dialog;
template <typename> struct Widget;
template <typename> struct Widget_interface;
template <typename...> struct Hosted;
}
namespace Dialog { namespace Meta {
template <typename, typename... TAIL>
struct Last { using Type = typename Last<TAIL...>::Type; };
template <typename T>
struct Last<T> { using Type = T; };
template <typename... ARGS> struct List
{
template <typename LAST>
struct Appended { using Result = List<ARGS..., LAST>; };
};
template <typename T1, typename T2>
struct Same { static constexpr bool VALUE = false; };
template <typename T>
struct Same<T, T> { static constexpr bool VALUE = true; };
} } /* namespace Dialog::Meta */
struct Dialog::Id
{
using Value = String<20>;
Value value;
bool operator == (Id const &other) const { return value == other.value; }
bool valid() const { return value.length() > 1; }
void print(Output &out) const { Genode::print(out, value); }
static Id from_xml(Xml_node const &node)
{
return Id { node.attribute_value("name", Value()) };
}
};
struct Dialog::Event : Noncopyable
{
/**
* ID of input-event sequence
*
* A sequence number refers to a sequence of consecutive events that
* belong together, e.g., all key events occurring while one key is held,
* or all touch motions while keeping the display touched.
*/
struct Seq_number
{
unsigned value;
bool operator == (Seq_number const &other) const { return value == other.value; }
};
struct Dragged
{
bool value; /* true after click and before clack */
};
Seq_number const seq_number;
Input::Event const event;
Event(Seq_number seq_number, Input::Event event)
: seq_number(seq_number), event(event) { }
void print(Output &out) const
{
Genode::print(out, seq_number.value, " ", event);
}
};
struct Dialog::At : Noncopyable
{
Event::Seq_number const seq_number;
Xml_node const &_location; /* widget hierarchy as found in hover reports */
bool const _valid = _location.has_attribute("name");
At(Event::Seq_number const seq_number, Xml_node const &location)
: seq_number(seq_number), _location(location) { }
/*
* The last element is not interpreted as widget type. It is preserved
* to denote the type of a 'Scoped' sub dialog.
*/
template <typename...>
struct Narrowed;
template <typename HEAD, typename... TAIL>
struct Narrowed<HEAD, TAIL...>
{
template <typename AT, typename FN>
static void with_at(AT const &at, FN const &fn)
{
HEAD::with_narrowed_at(at, [&] (AT const &narrowed_at) {
Narrowed<TAIL...>::with_at(narrowed_at, fn); });
}
};
template <typename LAST_IGNORED>
struct Narrowed<LAST_IGNORED>
{
template <typename AT, typename FN>
static void with_at(AT const &at, FN const &fn) { fn(at); }
};
template <typename... ARGS>
Id matching_id() const
{
struct Ignored { };
Id result { };
Narrowed<ARGS..., Ignored>::with_at(*this, [&] (At const &at) {
result = Id::from_xml(at._location); });
return result;
}
template <typename... HIERARCHY>
bool matches(Id const &id) const
{
return matching_id<HIERARCHY...>().value == id.value;
}
bool matches(Event::Seq_number const &s) const
{
return s.value == seq_number.value;
}
Id id() const { return Id::from_xml(_location); }
void print(Output &out) const { Genode::print(out, _location); }
};
struct Dialog::Clicked_at : At { using At::At; };
struct Dialog::Clacked_at : At { using At::At; };
struct Dialog::Dragged_at : At { using At::At; };
static inline Dialog::Clicked_at const &Dialog::clicked_at(At const &at)
{
return static_cast<Clicked_at const &>(at);
}
/**
* Tag for marking types as sub scopes
*
* This is a precaution to detect the use of wrong types as 'Scope::sub_scope'
* argument.
*/
class Dialog::Sub_scope
{
private: Sub_scope(); /* sub scopes cannot be instantiated */
};
template <typename... HIERARCHY>
struct Dialog::Scope : Noncopyable
{
using Hierarchy = Meta::List<HIERARCHY...>;
Id const id;
Xml_generator &xml;
At const &hover;
Event::Dragged const _dragged;
unsigned _sub_scope_count = 0;
Scope(Xml_generator &xml, At const &hover, Event::Dragged const dragged, Id const id)
: id(id), xml(xml), hover(hover), _dragged(dragged) { }
bool dragged() const { return _dragged.value; };
template <typename T, typename... ARGS>
void sub_scope(Id const id, ARGS &&... args)
{
/* create new 'Scope' type with 'T' appended */
using Sub_scope = Scope<HIERARCHY..., T>;
bool generated = false;
/* narrow hover information according to sub-scope type */
T::with_narrowed_at(hover, [&] (At const &narrowed_hover) {
if (id == Id::from_xml(narrowed_hover._location)) {
Sub_scope sub_scope { xml, narrowed_hover, _dragged, id };
T::view_sub_scope(sub_scope, args...);
generated = true;
}
});
if (generated)
return;
static Xml_node unhovered_xml { "<hover/>" };
At const unhovered_at { hover.seq_number, unhovered_xml };
Sub_scope sub_scope { xml, unhovered_at, _dragged, id };
T::view_sub_scope(sub_scope, args...);
}
template <typename T, typename... ARGS>
void sub_scope(ARGS &&... args)
{
static_assert(static_cast<Sub_scope *>((T *)(nullptr)) == nullptr,
"sub_scope called with type that is not a 'Sub_scope'");
sub_scope<T>(Id{_sub_scope_count++}, args...);
}
template <typename HOSTED, typename... ARGS>
void widget(HOSTED &hosted, ARGS &&... args)
{
hosted._view_hosted(*this, args...);
}
template <typename... ARGS>
bool hovered(Id const &id) const
{
return hover.matching_id<ARGS...>() == id;
}
bool hovered() const { return hover._valid; }
template <typename TYPE, typename FN>
void node(TYPE const &type, FN const &fn)
{
xml.node(type, [&] {
xml.attribute("name", id.value);
fn();
});
}
template <typename TYPE, typename FN>
void sub_node(TYPE const &type, FN const &fn)
{
xml.node(type, [&] { fn(); });
}
template <typename TYPE, typename NAME, typename FN>
void named_sub_node(TYPE const &type, NAME const &name, FN const &fn)
{
xml.node(type, [&] {
xml.attribute("name", name);
fn(); });
}
template <typename NAME, typename VALUE>
void attribute(NAME const &name, VALUE const &value)
{
xml.attribute(name, value);
}
template <typename FN>
void as_new_scope(FN const &fn) { fn(*reinterpret_cast<Scope<>*>(this)); }
};
template <typename COMPOUND_SUB_SCOPE>
struct Dialog::Widget : Noncopyable
{
static_assert(static_cast<Sub_scope *>((COMPOUND_SUB_SCOPE *)(nullptr)) == nullptr,
"'Widget' argument must be 'Sub_scope' type");
using Compound_sub_scope = COMPOUND_SUB_SCOPE;
template <typename AT, typename FN>
static void with_narrowed_at(AT const &at, FN const &fn) {
Compound_sub_scope::with_narrowed_at(at, fn); };
};
template <typename COMPOUND_SUB_SCOPE>
struct Dialog::Widget_interface : Noncopyable, Interface
{
using Compound_sub_scope = COMPOUND_SUB_SCOPE;
template <typename AT, typename FN>
static void with_narrowed_at(AT const &at, FN const &fn) {
Compound_sub_scope::with_narrowed_at(at, fn); };
};
template <typename... HIERARCHY>
struct Dialog::Hosted : Meta::Last<HIERARCHY...>::Type
{
Id const id;
using Widget = typename Meta::Last<HIERARCHY...>::Type;
using Compound_sub_scope = typename Widget::Compound_sub_scope;
template <typename... ARGS>
Hosted(Id const &id, ARGS &&... args) : Widget(args...), id(id) { }
/*
* \noapi helper for 'propagate' methods
*/
template <typename AT, typename FN>
void _with_narrowed_at(AT const &at, FN const &fn) const
{
At::Narrowed<HIERARCHY...>::with_at(at, [&] (AT const &narrowed) {
if (narrowed.template matches<Compound_sub_scope>(id))
fn(narrowed); });
}
template <typename... ARGS>
void propagate(Clicked_at const &at, ARGS &&... args)
{
_with_narrowed_at(at, [&] (auto const &at) { this->click(at, args...); });
}
template <typename... ARGS>
void propagate(Clacked_at const &at, ARGS &&... args)
{
_with_narrowed_at(at, [&] (auto const &at) { this->clack(at, args...); });
}
template <typename... ARGS>
void propagate(Dragged_at const &at, ARGS &&... args)
{
_with_narrowed_at(at, [&] (auto const &at) { this->drag(at, args...); });
}
/*
* \noapi used internally by 'Scope::widget'
*/
template <typename SCOPE, typename... ARGS>
void _view_hosted(SCOPE &scope, ARGS &&... args) const
{
using Call_structure = typename SCOPE::Hierarchy::Appended<Widget>::Result;
constexpr bool call_structure_matches_scoped_hierarchy =
Meta::Same<Meta::List<HIERARCHY...>, Call_structure>::VALUE;
static_assert(call_structure_matches_scoped_hierarchy,
"'view' call structure contradicts 'Scoped' hierarchy");
scope.as_new_scope([&] (Scope<> &s) {
s.sub_scope<Compound_sub_scope>(id, [&] (Scope<Compound_sub_scope> &s) {
Widget::view(s, args...); }); });
}
};
struct Dialog::Top_level_dialog : Interface, Noncopyable
{
using Name = String<20>;
Name const name;
Top_level_dialog(Name const &name) : name(name) { }
virtual void view(Scope<> &) const = 0;
virtual void click(Clicked_at const &) { };
virtual void clack(Clacked_at const &) { };
virtual void drag (Dragged_at const &) { };
};
#endif /* _INCLUDE__DIALOG__TYPES_H_ */

View File

@ -0,0 +1,139 @@
/*
* \brief Widget types
* \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.
*/
#ifndef _INCLUDE__DIALOG__WIDGETS_H_
#define _INCLUDE__DIALOG__WIDGETS_H_
#include <dialog/sub_scopes.h>
namespace Dialog {
template <typename> struct Select_button;
struct Toggle_button;
struct Action_button;
struct Deferred_action_button;
}
struct Dialog::Toggle_button : Widget<Button>
{
template <typename FN>
void view(Scope<Button> &s, bool selected, FN const &fn) const
{
bool const hovered = (s.hovered() && (!s.dragged() || selected));
if (selected) s.attribute("selected", "yes");
if (hovered) s.attribute("hovered", "yes");
fn(s);
}
void view(Scope<Button> &s, bool selected) const
{
view(s, selected, [&] (Scope<Button> &s) {
s.sub_scope<Dialog::Label>(s.id.value); });
}
template <typename FN>
void click(Clicked_at const &, FN const &toggle_fn) { toggle_fn(); }
};
template <typename ENUM>
struct Dialog::Select_button : Widget<Button>
{
ENUM const _value;
Select_button(ENUM value) : _value(value) { }
void view(Scope<Button> &s, ENUM selected_value) const
{
bool const selected = (selected_value == _value),
hovered = (s.hovered() && !s.dragged() && !selected);
if (selected) s.attribute("selected", "yes");
if (hovered) s.attribute("hovered", "yes");
s.sub_scope<Dialog::Label>(s.id.value);
}
template <typename FN>
void click(Clicked_at const &, FN const &select_fn) { select_fn(); }
};
struct Dialog::Action_button : Widget<Button>
{
Event::Seq_number _seq_number { };
template <typename FN>
void view(Scope<Button> &s, FN const &fn) const
{
bool const selected = _seq_number == s.hover.seq_number,
hovered = (s.hovered() && (!s.dragged() || selected));
if (selected) s.attribute("selected", "yes");
if (hovered) s.attribute("hovered", "yes");
fn(s);
}
void view(Scope<Button> &s) const
{
view(s, [&] (Scope<Button> &s) { s.sub_scope<Label>(s.id.value); });
}
template <typename FN>
void click(Clicked_at const &at, FN const &activate_fn)
{
_seq_number = at.seq_number;
activate_fn();
}
};
struct Dialog::Deferred_action_button : Widget<Button>
{
Event::Seq_number _seq_number { }; /* remembered at proposal time */
template <typename FN>
void view(Scope<Button> &s, FN const &fn) const
{
bool const selected = s.hovered() && s.dragged() && s.hover.matches(_seq_number),
hovered = s.hovered() && (!s.dragged() || selected);
if (selected) s.attribute("selected", "yes");
if (hovered) s.attribute("hovered", "yes");
fn(s);
}
void view(Scope<Button> &s) const
{
view(s, [&] (Scope<Button> &s) { s.sub_scope<Label>(s.id.value); });
}
void click(Clicked_at const &at)
{
_seq_number = at.seq_number;
}
template <typename FN>
void clack(Clacked_at const &at, FN const &activate_fn)
{
if (at.matches(_seq_number))
activate_fn();
}
};
#endif /* _INCLUDE__DIALOG__WIDGETS_H_ */

View File

@ -0,0 +1,4 @@
SRC_CC += sandboxed_runtime.cc
LIBS += sandbox
vpath %.cc $(REP_DIR)/src/lib/dialog

115
repos/gems/run/dialog.run Normal file
View File

@ -0,0 +1,115 @@
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 {
<config>
<parent-provides>
<service name="PD"/>
<service name="CPU"/>
<service name="ROM"/>
<service name="RM"/>
<service name="LOG"/>
<service name="IRQ"/>
<service name="IO_MEM"/>
<service name="IO_PORT"/>
</parent-provides>
<default caps="100"/>
<default-route>
<any-service> <parent/> <any-child/> </any-service>
</default-route>
<start name="timer">
<resource name="RAM" quantum="1M"/>
<provides><service name="Timer"/></provides>
</start>
<start name="drivers" caps="1500" managing_system="yes">
<resource name="RAM" quantum="64M"/>
<binary name="init"/>
<route>
<service name="ROM" label="config"> <parent label="drivers.config"/> </service>
<service name="Timer"> <child name="timer"/> </service>
<service name="Capture"> <child name="nitpicker"/> </service>
<service name="Event"> <child name="nitpicker"/> </service>
<any-service> <parent/> </any-service>
</route>
</start>
<start name="report_rom">
<resource name="RAM" quantum="1M"/>
<provides> <service name="Report"/> <service name="ROM"/> </provides>
<config verbose="yes">
<policy label="text_area.1 -> hover" report="nitpicker -> hover"/>
<policy label="text_area.2 -> clipboard" report="text_area.2 -> clipboard"/>
</config>
</start>
<start name="nitpicker">
<resource name="RAM" quantum="4M"/>
<provides>
<service name="Gui"/> <service name="Capture"/> <service name="Event"/>
</provides>
<config focus="rom">
<capture/> <event/>
<report hover="yes"/>
<background color="#123456"/>
<domain name="pointer" layer="1" content="client" label="no" origin="pointer" />
<domain name="default" layer="3" content="client" label="no" hover="always" />
<domain name="second" layer="2" xpos="200" ypos="300" content="client" label="no" hover="always" />
<policy label_prefix="pointer" domain="pointer"/>
<policy label_prefix="text_area.2" domain="second"/>
<default-policy domain="default"/>
</config>
</start>
<start name="pointer">
<resource name="RAM" quantum="1M"/>
<route>
<service name="Gui"> <child name="nitpicker" /> </service>
<any-service> <parent/> <any-child/> </any-service>
</route>
</start>
<start name="fonts_fs" caps="300">
<resource name="RAM" quantum="8M"/>
<binary name="vfs"/>
<route>
<service name="ROM" label="config"> <parent label="fonts_fs.config"/> </service>
<any-service> <parent/> </any-service>
</route>
<provides> <service name="File_system"/> </provides>
</start>
<start name="test-dialog" caps="1000">
<resource name="RAM" quantum="8M"/>
<config/>
<route>
<service name="ROM" label="hover"> <child name="report_rom"/> </service>
<any-service> <parent/> <any-child/> </any-service>
</route>
</start>
</config>}
set fd [open [run_dir]/genode/focus w]
puts $fd "<focus label=\"test-dialog -> \"/>"
close $fd
build { test/dialog app/menu_view }
build_boot_image [build_artifacts]
run_genode_until forever

View File

@ -0,0 +1,409 @@
/*
* \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/sandboxed_runtime.h>
#include <base/attached_ram_dataspace.h>
#include <gui_session/connection.h>
#include <input/component.h>
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;
}
struct Sandboxed_runtime::Gui_session : Session_object<Gui::Session>
{
Env &_env;
View &_view;
Registry<Gui_session>::Element _element;
using View_capability = Gui::View_capability;
Gui::Connection _connection;
Input::Session_component _input_component { _env, _env.ram() };
Signal_handler<Gui_session> _input_handler {
_env.ep(), *this, &Gui_session::_handle_input };
bool _clicked = false;
void _handle_input()
{
_connection.input()->for_each_event([&] (Input::Event const &ev) {
/*
* Assign new event sequence number, pass seq event to menu view
* to ensure freshness of hover information.
*/
bool const orig_clicked = _clicked;
if (click(ev)) _clicked = true;
if (clack(ev)) _clicked = false;
if (orig_clicked != _clicked) {
_view._global_seq_number.value++;
_input_component.submit(Input::Seq_number { _view._global_seq_number.value });
}
/* local event (click/clack) handling */
_view._handle_input_event(ev);
/* forward event to menu_view */
_input_component.submit(ev);
});
}
template <typename... ARGS>
Gui_session(Env &env, View &view, ARGS &&... args)
:
Session_object(args...),
_env(env), _view(view),
_element(_view._gui_sessions, *this),
_connection(env, _label.string())
{
_connection.input()->sigh(_input_handler);
_env.ep().manage(_input_component);
_input_component.event_queue().enabled(true);
}
~Gui_session() { _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<Gui::Session> session) override {
_connection.focus(session); }
};
Sandboxed_runtime::Sandboxed_runtime(Env &env, Allocator &alloc, Sandbox &sandbox)
:
_env(env), _alloc(alloc), _sandbox(sandbox),
_gui_service (_sandbox, _gui_handler),
_rom_service (_sandbox, _rom_handler),
_report_service(_sandbox, _report_handler)
{ }
bool Sandboxed_runtime::apply_sandbox_state(Xml_node const &state)
{
bool reconfiguration_needed = false;
state.for_each_sub_node("child", [&] (Xml_node const &child) {
using Name = Top_level_dialog::Name;
Name const name = child.attribute_value("name", Name());
_views.with_element(name,
[&] (View &view) {
if (view._menu_view_state.apply_child_state_report(child))
reconfiguration_needed = true; },
[&] /* no view named after this child */ { });
});
return reconfiguration_needed;
}
void Sandboxed_runtime::_handle_rom_service()
{
_rom_service.for_each_requested_session([&] (Rom_service::Request &request) {
if (request.label.last_element() == "dialog") {
_views.with_element(request.label.prefix(),
[&] (View &view) {
request.deliver_session(view._dialog_rom_session); },
[&] /* no view named after this child */ { });
}
});
_rom_service.for_each_session_to_close([&] (Dynamic_rom_session &) {
warning("closing of Dynamic_rom_session session not handled");
return Rom_service::Close_response::CLOSED;
});
}
void Sandboxed_runtime::_handle_report_service()
{
_report_service.for_each_requested_session([&] (Report_service::Request &request) {
if (request.label.last_element() == "hover") {
_views.with_element(request.label.prefix(),
[&] (View &view) {
view._hover_report_session.construct(_env, view._hover_handler, _env.ep(),
request.resources, "", request.diag);
request.deliver_session(*view._hover_report_session);
},
[&] /* no view named after this child */ { });
}
});
_report_service.for_each_session_to_close([&] (Report_session &) {
warning("closing of Report_session not handled");
return Report_service::Close_response::CLOSED;
});
}
void Sandboxed_runtime::_handle_gui_service()
{
_gui_service.for_each_requested_session([&] (Gui_service::Request &request) {
_views.with_element(request.label.prefix(),
[&] (View &view) {
Gui_session &session = *new (_alloc)
Gui_session(_env, view, _env.ep(),
request.resources, "", request.diag);
request.deliver_session(session);
},
[&] {
warning("unexpected GUI-sesssion request, label=", request.label);
});
});
_gui_service.for_each_upgraded_session([&] (Gui_session &session,
Session::Resources const &amount) {
session.upgrade(amount);
return Gui_service::Upgrade_response::CONFIRMED;
});
_gui_service.for_each_session_to_close([&] (Gui_session &session) {
destroy(_alloc, &session);
return Gui_service::Close_response::CLOSED;
});
}
void Sandboxed_runtime::gen_start_nodes(Xml_generator &xml) const
{
_views.for_each([&] (View const &view) {
view._menu_view_state.gen_start_node(xml); });
}
void Sandboxed_runtime::View::Menu_view_state::gen_start_node(Xml_generator &xml) const
{
xml.node("start", [&] () {
xml.attribute("name", _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("config", [&] () {
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", "Gui");
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", [&] () { }); });
});
});
}
void Sandboxed_runtime::View::_handle_input_event(Input::Event const &event)
{
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 Sandboxed_runtime::View::_handle_hover()
{
bool const orig_dialog_hovered = _dialog_hovered;
if (_hover_report_session.constructed())
_hover_report_session->with_xml([&] (Xml_node const &hover) {
_hover_seq_number = { hover.attribute_value("seq_number", 0U) };
_dialog_hovered = (hover.num_sub_nodes() > 0);
});
if (orig_dialog_hovered != _dialog_hovered || _dialog_hovered)
_dialog_rom_session.trigger_update();
if (_click_delivered && _click_seq_number.constructed()) {
_with_dialog_hover([&] (Xml_node const &hover) {
Dragged_at at(*_click_seq_number, hover);
_dialog.drag(at);
});
}
_try_handle_click_and_clack();
}
void Sandboxed_runtime::View::_try_handle_click_and_clack()
{
Constructible<Event::Seq_number> &click = _click_seq_number,
&clack = _clack_seq_number;
if (!_click_delivered && click.constructed() && *click == _hover_seq_number) {
_with_dialog_hover([&] (Xml_node const &hover) {
Clicked_at at(*click, hover);
_dialog.click(at);
_click_delivered = true;
});
}
if (click.constructed() && clack.constructed() && *clack== _hover_seq_number) {
_with_dialog_hover([&] (Xml_node const &hover) {
/* use click seq number for to associate clack with click */
Clacked_at at(*click, hover);
_dialog.clack(at);
});
click.destruct();
clack.destruct();
}
}
Sandboxed_runtime::View::~View()
{
_gui_sessions.for_each([&] (Gui_session &session) {
destroy(_alloc, &session); });
}

View File

@ -0,0 +1,134 @@
/*
* \brief Test for Genode's dialog API
* \author Norman Feske
* \date 2023-03-25
*/
/*
* 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 <base/component.h>
#include <dialog/runtime.h>
#include <dialog/widgets.h>
namespace Dialog_test {
using namespace Dialog;
struct Main;
}
struct Dialog_test::Main
{
Env &_env;
Heap _heap { _env.ram(), _env.rm() };
Runtime _runtime { _env, _heap };
struct Main_dialog : Top_level_dialog
{
Hosted<Vbox, Action_button> _inspect { Id { "Inspect" } };
Hosted<Vbox, Deferred_action_button> _confirm { Id { "Confirm" } };
Hosted<Vbox, Deferred_action_button> _cancel { Id { "Cancel" } };
enum class Payment { CASH, CARD } _payment = Payment::CASH;
using Payment_button = Select_button<Payment>;
Hosted<Vbox, Hbox, Payment_button> _cash { Id { "Cash" }, Payment::CASH },
_card { Id { "Card" }, Payment::CARD };
Main_dialog(Name const &name) : Top_level_dialog(name) { }
struct Dishes : Widget<Vbox>
{
Id _items[4] { { "Pizza" }, { "Salad" }, { "Pasta" }, { "Soup" } };
Id selected_item { };
void view(Scope<Vbox> &s) const
{
for (Id const &id : _items) {
s.sub_scope<Button>(id, [&] (Scope<Vbox, Button> &s) {
bool const selected = (id == selected_item),
hovered = (s.hovered() && (!s.dragged() || selected));
if (selected) s.attribute("selected", "yes");
if (hovered) s.attribute("hovered", "yes");
s.sub_scope<Label>(id.value);
});
}
}
void click(Clicked_at const &at)
{
for (Id const &id : _items)
if (at.matches<Vbox, Button>(id))
selected_item = id;
}
};
Hosted<Vbox, Frame, Dishes> _dishes { Id { "dishes" } };
void view(Scope<> &s) const override
{
s.sub_scope<Vbox>([&] (Scope<Vbox> &s) {
s.sub_scope<Min_ex>(15);
s.sub_scope<Frame>([&] (Scope<Vbox, Frame> &s) {
s.widget(_dishes); });
if (_dishes.selected_item.valid()) {
s.widget(_inspect);
s.sub_scope<Hbox>([&] (Scope<Vbox, Hbox> &s) {
s.widget(_cash, _payment);
s.widget(_card, _payment);
});
s.widget(_confirm);
s.widget(_cancel);
}
});
}
void click(Clicked_at const &at) override
{
_dishes .propagate(at);
_inspect.propagate(at, [&] { log("inspect activated!"); });
_confirm.propagate(at);
_cancel .propagate(at);
_cash .propagate(at, [&] { _payment = Payment::CASH; });
_card .propagate(at, [&] { _payment = Payment::CARD; });
}
void clack(Clacked_at const &at) override
{
_confirm.propagate(at, [&] { log("confirm activated!"); });
_cancel .propagate(at, [&] { _dishes.selected_item = { }; });
}
} _main_dialog { "main" };
Runtime::View _main_view { _runtime, _main_dialog };
/* handler used to respond to keyboard input */
Runtime::Event_handler<Main> _event_handler { _runtime, *this, &Main::_handle_event };
void _handle_event(Dialog::Event const &event)
{
log("_handle_event: ", event);
}
Main(Env &env) : _env(env) { }
};
void Component::construct(Genode::Env &env)
{
static Dialog_test::Main main(env);
}

View File

@ -0,0 +1,3 @@
TARGET = test-dialog
SRC_CC = main.cc
LIBS += base dialog