mirror of
https://github.com/genodelabs/genode.git
synced 2025-02-20 01:36:22 +00:00
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:
parent
6895175764
commit
4fdc999087
110
repos/gems/include/dialog/runtime.h
Normal file
110
repos/gems/include/dialog/runtime.h
Normal 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_ */
|
378
repos/gems/include/dialog/sandboxed_runtime.h
Normal file
378
repos/gems/include/dialog/sandboxed_runtime.h
Normal 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_ */
|
171
repos/gems/include/dialog/sub_scopes.h
Normal file
171
repos/gems/include/dialog/sub_scopes.h
Normal 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_ */
|
410
repos/gems/include/dialog/types.h
Normal file
410
repos/gems/include/dialog/types.h
Normal 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_ */
|
139
repos/gems/include/dialog/widgets.h
Normal file
139
repos/gems/include/dialog/widgets.h
Normal 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_ */
|
4
repos/gems/lib/mk/dialog.mk
Normal file
4
repos/gems/lib/mk/dialog.mk
Normal 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
115
repos/gems/run/dialog.run
Normal 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
|
409
repos/gems/src/lib/dialog/sandboxed_runtime.cc
Normal file
409
repos/gems/src/lib/dialog/sandboxed_runtime.cc
Normal 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); });
|
||||
}
|
134
repos/gems/src/test/dialog/main.cc
Normal file
134
repos/gems/src/test/dialog/main.cc
Normal 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);
|
||||
}
|
||||
|
3
repos/gems/src/test/dialog/target.mk
Normal file
3
repos/gems/src/test/dialog/target.mk
Normal file
@ -0,0 +1,3 @@
|
||||
TARGET = test-dialog
|
||||
SRC_CC = main.cc
|
||||
LIBS += base dialog
|
Loading…
x
Reference in New Issue
Block a user