Mobile version of Sculpt OS

This patch contains the mobile variant of Sculpt OS, which evolved
at the genode-allwinner repository until now. In consists of the
following parts:

- gems/src/app/phone_manager  plays the role of the sculpt manager
- sculpt/phone-linux          allows for test driving the mobile
                              variant on base-linux
- gems/src/app/dummy_modem    mockup of a modem's behavior, used for
                              GUI development and testing

The parts targeting a specific device (PinePhone) remain local to
the genode-allwinner repository.

To give it a try:

  make run/sculpt_test KERNEL=linux BOARD=linux \
                       SCULPT=phone LOG=core DEPOT=tar

Fixes #5125
This commit is contained in:
Norman Feske 2024-02-21 14:42:23 +01:00 committed by Christian Helmuth
parent ea51f1ffda
commit b370591e64
37 changed files with 5872 additions and 0 deletions

View File

@ -0,0 +1,63 @@
<config>
<parent-provides>
<service name="ROM"/>
<service name="PD"/>
<service name="CPU"/>
<service name="LOG"/>
<service name="Timer"/>
<service name="Report"/>
<service name="Capture"/>
<service name="Event"/>
</parent-provides>
<start name="fb_sdl" caps="100" ld="no">
<resource name="RAM" quantum="10M"/>
<route>
<service name="Event"> <child name="event_filter" label="sdl"/> </service>
<any-service> <parent/> </any-service>
</route>
</start>
<!-- toggle key mappings depending on the numlock state -->
<start name="numlock_remap_rom" caps="100">
<binary name="rom_filter"/>
<resource name="RAM" quantum="1M"/>
<provides> <service name="ROM"/> </provides>
<route>
<service name="ROM" label="config"> <parent label="numlock_remap.config"/> </service>
<service name="ROM" label="numlock"> <parent label="numlock"/> </service>
<any-service> <parent/> </any-service>
</route>
</start>
<start name="event_filter" caps="120">
<resource name="RAM" quantum="2M"/>
<provides> <service name="Event"/> </provides>
<route>
<service name="ROM" label="config"> <parent label="event_filter.config"/> </service>
<service name="ROM" label="numlock.remap"> <child name="numlock_remap_rom"/> </service>
<service name="ROM" label="capslock"> <parent label="capslock"/> </service>
<service name="ROM"> <parent/> </service>
<service name="PD"> <parent/> </service>
<service name="CPU"> <parent/> </service>
<service name="LOG"> <parent/> </service>
<service name="Timer"> <parent/> </service>
<service name="Event"> <parent/> </service>
</route>
</start>
<start name="modem" caps="120">
<binary name="dummy_modem"/>
<resource name="RAM" quantum="1M"/>
<route>
<service name="ROM" label="config"> <parent label="modem.config"/> </service>
<service name="ROM"> <parent/> </service>
<service name="PD"> <parent/> </service>
<service name="CPU"> <parent/> </service>
<service name="LOG"> <parent/> </service>
<service name="Timer"> <parent/> </service>
<service name="Report"> <parent/> </service>
</route>
</start>
</config>

View File

@ -0,0 +1,9 @@
<config>
<output>
<touch-key>
<input name="sdl"/>
<tap xpos="0" ypos="400" width="25" height="600" key="KEY_DASHBOARD"/>
</touch-key>
</output>
<policy label="sdl" input="sdl"/>
</config>

View File

@ -0,0 +1,21 @@
<config>
<vfs>
<rom name="Vera.ttf"/>
<rom name="VeraMono.ttf"/>
<dir name="fonts">
<dir name="title">
<ttf name="regular" path="/Vera.ttf" size_px="32" cache="256K"/>
</dir>
<dir name="text">
<ttf name="regular" path="/Vera.ttf" size_px="24" cache="256K"/>
</dir>
<dir name="annotation">
<ttf name="regular" path="/Vera.ttf" size_px="20" cache="256K"/>
</dir>
<dir name="monospace">
<ttf name="regular" path="/VeraMono.ttf" size_px="22" cache="256K"/>
</dir>
</dir>
</vfs>
<default-policy root="/fonts"/>
</config>

View File

@ -0,0 +1,6 @@
<launcher pkg="nano3d">
<route>
<service name="Gui"> <parent/> </service>
</route>
<config shape="cube"/>
</launcher>

View File

@ -0,0 +1,15 @@
<launcher pkg="system_shell">
<route>
<service name="Gui"> <parent label="focus"/> </service>
<service name="File_system" label="fonts"> <child name="fonts_fs"/> </service>
<service name="File_system" label="target"> <child name="default_fs_rw"/> </service>
<service name="File_system" label="config"> <parent label="config"/> </service>
<service name="File_system" label="report"> <parent label="report"/> </service>
<service name="Report" label="clipboard"> <parent label="clipboard"/> </service>
<service name="ROM" label="clipboard"> <parent label="clipboard"/> </service>
<service name="ROM" label="vimrc"> <parent label="config -> vimrc"/> </service>
<service name="ROM" label_last="cached_fs_rom">
<parent label="cached_fs_rom"/> </service>
<service name="RM"> <parent/> </service>
</route>
</launcher>

View File

@ -0,0 +1,169 @@
<config>
<input name="leitzentrale_enabled" rom="leitzentrale" node="leitzentrale">
<attribute name="enabled" /> </input>
<output node="config">
<inline>
<parent-provides>
<service name="ROM"/>
<service name="PD"/>
<service name="RM"/>
<service name="CPU"/>
<service name="LOG"/>
<service name="Report"/>
<service name="Gui"/>
<service name="Timer"/>
<service name="File_system"/>
</parent-provides>
<default-route> <any-service> <parent/> </any-service> </default-route>
<default caps="100"/>
<service name="Gui">
<default-policy> <child name="wm"/> </default-policy> </service>
<service name="File_system">
<default-policy> <child name="fonts_fs"/> </default-policy> </service>
<service name="ROM">
<default-policy> <child name="report_rom"/> </default-policy> </service>
<service name="Report">
<default-policy> <child name="report_rom"/> </default-policy> </service>
<start name="report_rom" caps="200">
<resource name="RAM" quantum="4M"/>
<provides>
<service name="Report"/>
<service name="ROM"/>
</provides>
<config verbose="no">
<policy label="decorator -> window_layout" report="manager -> window_layout"/>
<policy label="wm -> resize_request" report="manager -> window_layout"/>
<policy label="wm -> focus" report="manager -> wm_focus"/>
<policy label="decorator -> pointer" report="wm -> pointer"/>
<policy label="manager -> window_list" report="wm -> window_list"/>
<policy label="manager -> decorator_margins" report="decorator -> decorator_margins"/>
<policy label="runtime -> leitzentrale -> main_view -> dialog"
report="manager -> main_dialog"/>
<policy label="manager -> main_view_hover"
report="runtime -> leitzentrale -> main_view -> hover"/>
</config>
</start>
<start name="wm" caps="300">
<resource name="RAM" quantum="4M"/>
<provides>
<service name="Gui"/> <service name="Report"/> <service name="ROM"/>
</provides>
<config>
<policy label_prefix="decorator" role="decorator"/>
<default-policy/>
</config>
<route>
<service name="ROM" label="resize_request"> <child name="report_rom"/> </service>
<service name="ROM" label="focus"> <child name="report_rom"/> </service>
<service name="Report"> <child name="report_rom"/> </service>
<service name="Gui"> <child name="manager"/> </service>
<any-service> <parent/> <any-child/> </any-service>
</route>
</start>
<start name="decorator" caps="350">
<binary name="themed_decorator"/>
<resource name="RAM" quantum="12M"/>
<resource name="CPU" quantum="20"/>
<config>
<libc/>
<vfs>
<dir name="theme">
<inline name="metadata">
<theme>
<aura top="5" bottom="5" left="5" right="5"/>
<decor top="6" bottom="6" left="6" right="6"/>
</theme>
</inline>
<rom name="default.png"/>
</dir>
<dir name="dev"> <log/> </dir>
</vfs>
<policy label="log" decoration="yes" motion="20"/>
<policy label="runtime -> leitzentrale -> main_view" decoration="no" motion="0"/>
<policy label="runtime -> leitzentrale -> touch_keyboard" decoration="no" motion="20"/>
<policy label_prefix="logo" decoration="no"/>
<default-policy/>
</config>
<route>
<service name="ROM" label="default.png"> <parent label="drop_shadow.png"/> </service>
<service name="ROM" label="window_layout"> <child name="report_rom"/> </service>
<service name="ROM" label="pointer"> <child name="report_rom"/> </service>
<service name="Report" label="decorator_margins"> <child name="report_rom"/> </service>
<service name="Report" label="hover"> <child name="report_rom"/> </service>
<service name="Gui"> <child name="wm"/> </service>
<any-service> <parent/> </any-service>
</route>
</start>
<start name="config_fs_report">
<binary name="fs_report"/>
<resource name="RAM" quantum="2M"/>
<provides> <service name="Report"/> </provides>
<config> <vfs> <fs/> </vfs> </config>
<route>
<service name="File_system"> <parent label="config"/> </service>
<any-service> <parent/> </any-service>
</route>
</start>
<start name="manager" caps="300">
<binary name="phone_manager"/>
<resource name="RAM" quantum="3M"/>
<provides> <service name="Gui"/> </provides>
<config ld_verbose="yes" verbose_modem="no"/>
<route>
<service name="Report" label="runtime_config">
<child name="config_fs_report" label="managed -> runtime"/> </service>
<service name="Report" label="deploy_config">
<child name="config_fs_report" label="managed -> deploy"/> </service>
<service name="Report" label="system_config">
<child name="config_fs_report" label="managed -> system"/> </service>
<service name="Report" label="wifi_config">
<child name="config_fs_report" label="managed -> wifi"/> </service>
<service name="Report" label="modem_config">
<child name="config_fs_report" label="managed -> modem"/> </service>
<service name="Report" label="audio_config">
<child name="config_fs_report" label="managed -> audio"/> </service>
<service name="Report" label="nic_router_config">
<child name="config_fs_report" label="managed -> nic_router"/> </service>
<service name="Report" label="installation_config">
<child name="config_fs_report" label="managed -> installation"/> </service>
<service name="Report" label="depot_query">
<child name="config_fs_report" label="managed -> depot_query"/> </service>
<service name="Report"> <child name="report_rom"/> </service>
<service name="ROM" label_prefix="report ->"> <parent/> </service>
<service name="ROM" label="nitpicker_focus"> <parent/> </service>
<service name="ROM" label="nitpicker_hover"> <parent/> </service>
<service name="ROM" label_suffix="_hover"> <child name="report_rom"/> </service>
<service name="ROM" label="window_list"> <child name="report_rom"/> </service>
<service name="ROM" label="decorator_margins"> <child name="report_rom"/> </service>
<service name="ROM" label="clicked"> <child name="report_rom"/> </service>
<service name="Gui"> <parent label="manager -> fader -> "/> </service>
<any-service> <parent/> </any-service>
</route>
</start>
<start name="fonts_fs" caps="100">
<binary name="vfs"/>
<resource name="RAM" quantum="4M"/>
<provides> <service name="File_system"/> </provides>
<route>
<service name="ROM" label="config">
<parent label="config -> fonts"/> </service>
<any-service> <parent/> </any-service>
</route>
</start>
</inline>
</output>
</config>

View File

@ -0,0 +1,28 @@
<config focus="rom">
<capture/> <event/>
<report hover="yes" focus="yes" clicked="yes" keystate="no"/>
<background color="#000000"/>
<domain name="overlay" layer="0" label="no" hover="always" focus="transient" content="client"/>
<domain name="pointer" layer="0" content="client" label="no" origin="pointer" />
<domain name="invisible" layer="1" xpos="10000" />
<domain name="leitzentrale" layer="1" label="no" hover="always" focus="transient" content="client"/>
<domain name="touch_keyboard" layer="2" ypos="960" label="no" hover="always" content="client" focus="transient" />
<domain name="background" layer="4" label="no" hover="always" content="client" focus="transient" />
<domain name="default" layer="3" label="no" hover="always" content="client" focus="transient" width="720" height="960"/>
<domain name="follow_touch" layer="3" origin="pointer" label="no" hover="always" content="client" focus="transient" width="720" height="960"/>
<domain name="camera" layer="2" label="no" hover="always" content="client" focus="transient" xpos="120" ypos="120"/>
<policy label_prefix="pointer -> " domain="invisible"/>
<policy label_prefix="leitzentrale -> " domain="leitzentrale"/>
<policy label_prefix="runtime -> touch_keyboard" domain="touch_keyboard"/>
<policy label_prefix="runtime -> follow_touch" domain="follow_touch"/>
<policy label_prefix="runtime -> camera" domain="camera"/>
<policy label_prefix="runtime -> overlay" domain="overlay"/>
<policy label_prefix="backdrop" domain="background"/>
<default-policy domain="default"/>
<global-key name="KEY_DASHBOARD" label="global_keys_handler -> input" />
<global-key name="KEY_PRINT" label="runtime -> screenshot -> keys -> input" />
<global-key name="KEY_POWER" label="leitzentrale -> manager -> fader -> " />
</config>

View File

@ -0,0 +1,12 @@
drivers: phone_linux
leitzentrale: phone
nitpicker: phone
fonts: phone
event_filter: phone_linux
build: app/phone_manager server/nitpicker app/menu_view server/wm
build: server/event_filter
build: app/dummy_modem
import: pkg/drivers_interactive-linux pkg/touch_keyboard
deploy: example
ram_fs: depot
launcher: sticks_blue_backdrop direct_nano3d direct_system_shell

View File

@ -0,0 +1,256 @@
/*
* \brief Simulation of a modem driver
* \author Norman Feske
* \date 2022-06-02
*/
/* Genode includes */
#include <base/component.h>
#include <base/attached_rom_dataspace.h>
#include <os/reporter.h>
#include <timer_session/connection.h>
namespace Dummy_modem {
using namespace Genode;
struct Main;
}
struct Dummy_modem::Main
{
Env &_env;
Expanding_reporter _reporter { _env, "modem", "state" };
enum class Power_state { UNKNOWN, OFF, STARTING_UP, ON, SHUTTING_DOWN };
Power_state _power_state = Power_state::ON;
unsigned _startup_seconds = 0;
unsigned _shutdown_seconds = 0;
struct Pin
{
enum class State { REQUIRED, CHECKING, OK, PUK_NEEDED };
State state = State::OK;
unsigned check_countdown_seconds = 0;
enum { INITIAL_REMAINING_ATTEMPTS = 3 };
unsigned remaining_attempts = INITIAL_REMAINING_ATTEMPTS;
using Code = String<10>;
Code current_code { };
Code failed_code { };
Pin() { }
};
Pin _pin { };
void _generate_report(Xml_generator &xml) const
{
auto power_value = [] (Power_state state)
{
switch (state) {
case Power_state::OFF: return "off";
case Power_state::STARTING_UP: return "starting up";
case Power_state::ON: return "on";
case Power_state::SHUTTING_DOWN: return "shutting down";
case Power_state::UNKNOWN: break;
}
return "";
};
xml.attribute("power", power_value(_power_state));
if (_power_state == Power_state::STARTING_UP)
xml.attribute("startup_seconds", _startup_seconds);
if (_power_state == Power_state::SHUTTING_DOWN)
xml.attribute("shutdown_seconds", _shutdown_seconds);
auto pin_value = [] (Pin::State state)
{
switch (state) {
case Pin::State::REQUIRED: return "required";
case Pin::State::CHECKING: return "checking";
case Pin::State::OK: return "ok";
case Pin::State::PUK_NEEDED: return "puk needed";
}
return "";
};
if (_power_state == Power_state::ON) {
xml.attribute("pin", pin_value(_pin.state));
if (_pin.remaining_attempts != Pin::INITIAL_REMAINING_ATTEMPTS)
xml.attribute("pin_remaining_attempts", _pin.remaining_attempts);
}
if (_pin.state == Pin::State::OK) {
enum Scenario { IDLE, INCOMING_CALL, INITIATED_CALL };
Scenario scenario = INITIATED_CALL;
if (scenario == INCOMING_CALL) {
xml.node("call", [&] {
xml.attribute("number", "+49123456789");
xml.attribute("state", "incoming");
});
}
if (scenario == INITIATED_CALL) {
xml.node("call", [&] {
xml.attribute("number", "+4911223344");
xml.attribute("state", "outbound");
});
}
}
}
void _update_state_report()
{
_reporter.generate([&] (Xml_generator &xml) {
_generate_report(xml); });
}
Timer::Connection _timer { _env };
Signal_handler<Main> _timer_handler {
_env.ep(), *this, &Main::_handle_timer };
Attached_rom_dataspace _config { _env, "config" };
Signal_handler<Main> _config_handler {
_env.ep(), *this, &Main::_handle_config };
void _handle_timer()
{
/* apply time-driven rules */
if (_power_state == Power_state::STARTING_UP) {
_startup_seconds++;
if (_startup_seconds > 4) {
_power_state = Power_state::ON;
_startup_seconds = 0;
}
}
if (_power_state == Power_state::SHUTTING_DOWN) {
_shutdown_seconds++;
if (_shutdown_seconds > 5) {
_power_state = Power_state::OFF;
_shutdown_seconds = 0;
_pin = Pin { };
}
}
switch (_pin.state) {
case Pin::State::REQUIRED:
case Pin::State::OK:
case Pin::State::PUK_NEEDED:
break;
case Pin::State::CHECKING:
if (_pin.check_countdown_seconds > 0)
_pin.check_countdown_seconds--;
if (_pin.check_countdown_seconds == 0) {
if (_pin.current_code == "1234") {
_pin.state = Pin::State::OK;
} else {
_pin.state = Pin::State::REQUIRED;
_pin.failed_code = _pin.current_code;
if (_pin.remaining_attempts == 0)
_pin.state = Pin::State::PUK_NEEDED;
else
_pin.remaining_attempts--;
}
} else {
_trigger_timer_in_one_second();
}
break;
}
/* re-apply rules dictated by the configuration */
_handle_config();
}
void _trigger_timer_in_one_second() { _timer.trigger_once(1000*1000UL); }
void _handle_config()
{
_config.update();
Xml_node const config = _config.xml();
log("_handle_config: ", config);
auto power_from_config = [&]
{
auto name = config.attribute_value("power", String<10>());
if (name == "on") return Power_state::ON;
if (name == "off") return Power_state::OFF;
return Power_state::UNKNOWN;
};
Power_state const requested_power_state = power_from_config();
if (requested_power_state != _power_state) {
if (requested_power_state == Power_state::ON) {
if (_power_state == Power_state::OFF)
_power_state = Power_state::STARTING_UP;
}
if (requested_power_state == Power_state::OFF) {
if (_power_state == Power_state::ON)
_power_state = Power_state::SHUTTING_DOWN;
}
if (requested_power_state != Power_state::UNKNOWN)
_trigger_timer_in_one_second();
}
switch (_pin.state) {
case Pin::State::REQUIRED:
if (config.has_attribute("pin")) {
Pin::Code const code = config.attribute_value("pin", Pin::Code());
/* don't re-submit failed pin */
if (code != _pin.failed_code) {
_pin.current_code = code;
_pin.state = Pin::State::CHECKING;
_pin.check_countdown_seconds = 2;
_trigger_timer_in_one_second();
}
}
break;
case Pin::State::CHECKING: /* handled by _handle_timer */
case Pin::State::OK:
case Pin::State::PUK_NEEDED:
break;
}
_update_state_report();
}
Main(Env &env) : _env(env)
{
_timer.sigh(_timer_handler);
_config.sigh(_config_handler);
_handle_config();
}
};
void Component::construct(Genode::Env &env)
{
static Dummy_modem::Main main(env);
}

View File

@ -0,0 +1,3 @@
TARGET := dummy_modem
SRC_CC := main.cc
LIBS := base

View File

@ -0,0 +1,30 @@
/*
* \brief Compile-time feature selection
* \author Norman Feske
* \date 2022-11-10
*/
/*
* Copyright (C) 2022 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 _FEATURE_H_
#define _FEATURE_H_
#include <types.h>
namespace Sculpt { struct Feature; };
struct Sculpt::Feature
{
static constexpr bool PRESENT_PLUS_MENU = false;
static constexpr bool STORAGE_DIALOG_HOSTED_IN_GRAPH = false;
static constexpr bool INSPECT_VIEW = false;
static constexpr bool VISUAL_HOVER = false;
};
#endif /* _FEATURE_H_ */

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
/*
* \brief Audio volume
* \author Norman Feske
* \date 2022-12-13
*/
/*
* Copyright (C) 2022 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 _MODEL__AUDIO_VOLUME_H_
#define _MODEL__AUDIO_VOLUME_H_
#include <types.h>
namespace Sculpt { struct Audio_volume { unsigned value; }; }
#endif /* _MODEL__AUDIO_VOLUME_H_ */

View File

@ -0,0 +1,215 @@
/*
* \brief State of the current call
* \author Norman Feske
* \date 2022-06-29
*/
/*
* Copyright (C) 2022 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 _MODEL__CURRENT_CALL_H_
#define _MODEL__CURRENT_CALL_H_
#include <model/modem_state.h>
namespace Sculpt { struct Current_call; }
struct Sculpt::Current_call
{
using Number = Modem_state::Number;
Number number { };
enum class State {
NONE,
/*
* Entered by user interaction
*/
ACCEPTED, /* picked up incoming call */
REJECTED, /* rejected incoming or active call */
INITIATED,
CANCELED, /* canced outbound call */
/*
* Entered by modem activity
*/
INCOMING,
HUNG_UP, /* disconnected by callee */
OUTBOUND, /* dialing */
ALERTING, /* ring at the callee */
ACTIVE,
};
State state = State::NONE;
/**
* Return true if the modem state is applicable to the current state
*/
bool _applicable(Modem_state const &modem) const
{
if (state == State::NONE)
return true;
/* accept state updates when current state already reflects the modem */
bool const entered_by_modem_activity = (state == State::INCOMING)
|| (state == State::HUNG_UP)
|| (state == State::OUTBOUND)
|| (state == State::ALERTING)
|| (state == State::ACTIVE);
if (entered_by_modem_activity)
return true;
/* an accepted incoming call became active */
if ((state == State::ACCEPTED) && modem.active_call())
return true;
/* forget last canceled initiated call when a new call comes in */
if ((state == State::CANCELED) && modem.incoming_call())
return true;
/* reset canceled state once the modem cleared the call */
if ((state == State::CANCELED) && !modem.any_call())
return true;
/* reset rejected state once the modem cleared the call */
if ((state == State::REJECTED) && !modem.any_call())
return true;
/* clear rejected state when called from a different number */
if ((state == State::REJECTED) && modem.incoming_call() && (modem.number() != number))
return true;
/* an initiated call is reflected by the modem as outbound or alerting */
if ((state == State::INITIATED) && modem.outbound_call())
return true;
/* an initiated call became active */
if ((state == State::INITIATED) && modem.active_call())
return true;
return false;
}
static State _from_modem_call_state(Modem_state::Call_state call_state)
{
switch (call_state) {
case Modem_state::Call_state::INCOMING: return State::INCOMING;
case Modem_state::Call_state::ACTIVE: return State::ACTIVE;
case Modem_state::Call_state::ALERTING: return State::ALERTING;
case Modem_state::Call_state::OUTBOUND: return State::OUTBOUND;
case Modem_state::Call_state::NONE: break;
};
return State::NONE;
}
bool speaker = false;
bool connecting() const
{
return state == State::INITIATED
|| state == State::OUTBOUND
|| state == State::ALERTING;
}
bool incoming() const { return state == State::INCOMING; }
bool accepted() const { return state == State::ACCEPTED; }
bool active() const { return state == State::ACTIVE; }
bool none() const { return state == State::NONE; }
bool canceled() const { return state == State::CANCELED; }
void accept()
{
if (state == State::INCOMING) {
state = State::ACCEPTED;
speaker = false;
}
}
void reject()
{
if (state == State::INCOMING || state == State::ACTIVE)
state = State::REJECTED;
speaker = false;
}
void initiate(Number const &n)
{
number = n;
state = State::INITIATED;
speaker = false;
}
void cancel()
{
state = State::CANCELED;
speaker = false;
}
void toggle_speaker()
{
speaker = !speaker;
}
void update(Modem_state const &modem)
{
if (_applicable(modem)) {
state = _from_modem_call_state(modem.call_state());
number = modem.number();
}
if (state == State::NONE)
speaker = false;
}
void gen_modem_config(Xml_generator &xml) const
{
xml.attribute("speaker", speaker ? "yes" : "no");
switch (state) {
case State::NONE:
case State::INCOMING:
break;
case State::ACCEPTED:
xml.node("call", [&] {
xml.attribute("number", number);
xml.attribute("state", "accepted");
});
break;
case State::REJECTED:
case State::HUNG_UP:
case State::CANCELED:
xml.node("call", [&] {
xml.attribute("number", number);
xml.attribute("state", "rejected");
});
break;
case State::INITIATED:
case State::OUTBOUND:
case State::ALERTING:
case State::ACTIVE:
xml.node("call", [&] {
xml.attribute("number", number);
});
break;
}
}
bool operator != (Current_call const &other) const
{
return (number != other.number)
|| (state != other.state)
|| (speaker != other.speaker);
}
};
#endif /* _MODEL__CURRENT_CALL_H_ */

View File

@ -0,0 +1,84 @@
/*
* \brief Dialed number
* \author Norman Feske
* \date 2018-05-23
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
#ifndef _MODEL__DIALED_NUMBER_H_
#define _MODEL__DIALED_NUMBER_H_
#include <base/output.h>
#include <util/utf8.h>
#include <types.h>
#include <xml.h>
namespace Sculpt { struct Dialed_number; }
struct Sculpt::Dialed_number : Noncopyable
{
public:
struct Digit
{
char value;
bool valid() const { return (value >= '0' && value <= '9')
|| (value == '#')
|| (value == '*'); }
void print(Output &out) const
{
if (valid())
Genode::print(out, Char(value));
}
};
private:
enum { CAPACITY = 32 };
Digit _digits[CAPACITY] { };
unsigned _length = 0;
public:
void print(Output &out) const
{
for (unsigned i = 0; i < _length; i++)
Genode::print(out, _digits[i]);
}
void append_digit(Digit d)
{
/* ignore out-of-range digit values */
if (!d.valid())
return;
if (_length < CAPACITY) {
_digits[_length] = d;
_length++;
}
}
void remove_last_digit()
{
if (_length > 0) {
_length--;
_digits[_length].value = 0;
}
}
bool suitable_for_call() const { return _length >= 3; }
bool at_least_one_digit() const { return _length > 0; }
};
#endif /* _MODEL__DIALED_NUMBER_H_ */

View File

@ -0,0 +1,21 @@
/*
* \brief Microphone state
* \author Norman Feske
* \date 2022-12-13
*/
/*
* Copyright (C) 2022 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 _MODEL__MIC_STATE_H_
#define _MODEL__MIC_STATE_H_
#include <types.h>
namespace Sculpt { enum class Mic_state { OFF, PHONE, ON }; }
#endif /* _MODEL__MIC_STATE_H_ */

View File

@ -0,0 +1,195 @@
/*
* \brief Modem state as retrieved from the modem driver
* \author Norman Feske
* \date 2022-05-20
*/
/*
* Copyright (C) 2022 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 _MODEL__MODEM_STATE_H_
#define _MODEL__MODEM_STATE_H_
namespace Sculpt { struct Modem_state; }
struct Sculpt::Modem_state
{
enum class Power { UNAVAILABLE, OFF, STARTING_UP, ON, SHUTTING_DOWN };
Power _power;
using Number = String<128>;
enum class Call_state { NONE, INCOMING, ACTIVE, OUTBOUND, ALERTING };
Call_state _call_state = Call_state::NONE;
Number _number;
unsigned _startup_seconds;
unsigned _shutdown_seconds;
enum class Pin_state { UNKNOWN, REQUIRED, CHECKING, REJECTED, OK, PUK_NEEDED };
Pin_state _pin_state;
unsigned _pin_remaining_attempts;
bool transient() const
{
return _power == Power::STARTING_UP
|| _power == Power::SHUTTING_DOWN;
}
bool on() const
{
return _power == Power::ON
|| _power == Power::STARTING_UP;
}
bool ready() const { return _power == Power::ON; }
Call_state call_state() const { return _call_state; }
Number number() const { return _number; }
bool any_call() const { return _call_state != Call_state::NONE; }
bool incoming_call() const { return _call_state == Call_state::INCOMING; }
bool outbound_call() const { return _call_state == Call_state::OUTBOUND
|| _call_state == Call_state::ALERTING; }
bool active_call() const { return _call_state == Call_state::ACTIVE; }
bool pin_required() const { return _pin_state == Pin_state::REQUIRED
|| _pin_state == Pin_state::REJECTED; }
bool pin_ok() const { return _pin_state == Pin_state::OK; }
bool pin_rejected() const { return _pin_state == Pin_state::REJECTED; }
using Power_message = String<128>;
Power_message power_message() const
{
using Msg = Power_message;
switch (_power) {
case Power::STARTING_UP: return Msg(" starting up (", _startup_seconds, ") ");
case Power::SHUTTING_DOWN: return Msg(" shutting down (", _shutdown_seconds, ") ");
case Power::ON:
switch (_pin_state) {
case Pin_state::REQUIRED:
return Msg(" PIN required");
case Pin_state::REJECTED:
return (_pin_remaining_attempts == 1)
? Msg(" PIN rejected (one more try) ")
: Msg(" PIN rejected (", _pin_remaining_attempts, " more tries) ");
case Pin_state::CHECKING:
return Msg(" checking PIN ... ");
case Pin_state::OK:
return Msg(" ready ");
case Pin_state::PUK_NEEDED:
return Msg(" PUK needed, giving up. ");
case Pin_state::UNKNOWN:
break;
}
return Msg(" unknown PIN state ");
case Power::OFF: return Msg(" powered off ");
case Power::UNAVAILABLE: break;
}
return Msg(" unavailable" );
}
bool operator != (Modem_state const &other) const
{
return other._power != _power
|| other._number != _number
|| other._call_state != _call_state
|| other._startup_seconds != _startup_seconds
|| other._shutdown_seconds != _shutdown_seconds
|| other._pin_state != _pin_state
|| other._pin_remaining_attempts != _pin_remaining_attempts;
}
static Modem_state from_xml(Xml_node const &node)
{
auto power = [&]
{
auto value = node.attribute_value("power", String<20>());
if (value == "on") return Power::ON;
if (value == "starting up") return Power::STARTING_UP;
if (value == "shutting down") return Power::SHUTTING_DOWN;
if (value == "off") return Power::OFF;
return Power::UNAVAILABLE;
};
auto pin_state = [&]
{
auto value = node.attribute_value("pin", String<20>());
if (value == "required")
return node.has_attribute("pin_remaining_attempts")
? Pin_state::REJECTED : Pin_state::REQUIRED;
if (value == "checking") return Pin_state::CHECKING;
if (value == "ok") return Pin_state::OK;
if (value == "puk needed") return Pin_state::PUK_NEEDED;
return Pin_state::UNKNOWN;
};
auto number = [&]
{
Number result { };
node.with_optional_sub_node("call", [&] (Xml_node const &call) {
result = call.attribute_value("number", Number()); });
return result;
};
auto call_state = [&]
{
Call_state result = Call_state::NONE;
node.with_optional_sub_node("call", [&] (Xml_node const &call) {
auto const type = call.attribute_value("state", String<20>());
result = (type == "incoming") ? Call_state::INCOMING
: (type == "active") ? Call_state::ACTIVE
: (type == "outbound") ? Call_state::OUTBOUND
: (type == "alerting") ? Call_state::ALERTING
: Call_state::NONE;
});
return result;
};
return Modem_state {
._power = power(),
._call_state = call_state(),
._number = number(),
._startup_seconds = node.attribute_value("startup_seconds", 0u),
._shutdown_seconds = node.attribute_value("shutdown_seconds", 0u),
._pin_state = pin_state(),
._pin_remaining_attempts = node.attribute_value("pin_remaining_attempts", 0u),
};
}
};
#endif /* _MODEL__MODEM_STATE_H_ */

View File

@ -0,0 +1,102 @@
/*
* \brief Power state as provided by the power driver
* \author Norman Feske
* \date 2022-11-09
*/
/*
* Copyright (C) 2022 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 _MODEL__POWER_STATE_H_
#define _MODEL__POWER_STATE_H_
#include <types.h>
namespace Sculpt { struct Power_state; }
struct Sculpt::Power_state
{
bool ac_present, battery_present, charging;
double voltage;
struct Battery
{
double charge_current, power_draw;
unsigned remaining_capacity;
static Battery from_xml(Xml_node const &battery)
{
return Battery {
.charge_current = battery.attribute_value("charge_current", 0.0),
.power_draw = battery.attribute_value("power_draw", 0.0),
.remaining_capacity = battery.attribute_value("remaining_capacity", 0u)
};
}
};
Battery battery;
enum class Profile { UNKNOWN, PERFORMANCE, ECONOMIC };
Profile profile;
unsigned brightness;
static Power_state from_xml(Xml_node const &node)
{
Battery battery { };
node.with_optional_sub_node("battery", [&] (Xml_node const &node) {
battery = Battery::from_xml(node);
});
auto profile_from_xml = [] (Xml_node const &node)
{
auto value = node.attribute_value("power_profile", String<64>());
if (value == "performance") return Profile::PERFORMANCE;
if (value == "economic") return Profile::ECONOMIC;
return Profile::UNKNOWN;
};
return Power_state {
.ac_present = node.attribute_value("ac_present", false),
.battery_present = node.has_sub_node("battery"),
.charging = node.attribute_value("charging", false),
.voltage = node.attribute_value("voltage", 0.0),
.battery = battery,
.profile = profile_from_xml(node),
.brightness = node.attribute_value("brightness", 0u),
};
}
using Summary = String<128>;
Summary summary() const
{
if (!battery_present)
return "AC";
return Summary(battery.remaining_capacity,"%", charging ? " +": "");
}
bool modem_present() const
{
/* assume presence of a modem before receiving the first report */
bool const uncertain = !ac_present && !battery_present;
if (uncertain)
return true;
return battery_present;
}
};
#endif /* _MODEL__POWER_STATE_H_ */

View File

@ -0,0 +1,123 @@
/*
* \brief SIM pin
* \author Norman Feske
* \date 2018-05-23
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
#ifndef _MODEL__SIM_PIN_H_
#define _MODEL__SIM_PIN_H_
#include <base/output.h>
#include <util/utf8.h>
#include <types.h>
#include <xml.h>
namespace Sculpt { struct Bullet; }
struct Sculpt::Bullet
{
void print(Output &out) const
{
char const bullet_utf8[4] = { (char)0xe2, (char)0x80, (char)0xa2, 0 };
Genode::print(out, bullet_utf8);
}
};
namespace Sculpt { struct Blind_sim_pin; }
struct Sculpt::Blind_sim_pin
{
virtual void print_bullets(Output &) const = 0;
virtual ~Blind_sim_pin() { }
void print(Output &out) const { print_bullets(out); }
virtual bool suitable_for_unlock() const = 0;
virtual bool at_least_one_digit() const = 0;
};
namespace Sculpt { struct Sim_pin; }
struct Sculpt::Sim_pin : Blind_sim_pin
{
public:
struct Digit { unsigned value; };
private:
enum { CAPACITY = 4 };
Digit _digits[CAPACITY] { };
unsigned _length = 0;
public:
bool confirmed = false;
void print(Output &out) const
{
for (unsigned i = 0; i < _length; i++)
Genode::print(out, _digits[i].value);
}
void append_digit(Digit d)
{
/* ignore out-of-range digit values */
if (d.value > 9)
return;
if (_length < CAPACITY) {
_digits[_length] = d;
_length++;
}
}
void remove_last_digit()
{
if (_length > 0) {
_length--;
_digits[_length].value = 0;
}
}
void print_bullets(Output &out) const override
{
for (unsigned i = 0; i < _length; i++)
Genode::print(out, Bullet());
}
bool suitable_for_unlock() const override { return _length == 4; }
bool at_least_one_digit() const override { return _length > 0; }
bool operator != (Sim_pin const &other) const
{
auto any_digit_differs = [&] (auto a, auto b)
{
for (unsigned i = 0; i < _length; i++)
if (a[i].value != b[i].value)
return true;
return false;
};
return (_length != other._length)
|| any_digit_differs(_digits, other._digits);
}
};
#endif /* _MODEL__SIM_PIN_H_ */

View File

@ -0,0 +1,89 @@
/*
* \brief XML configuration for spawning the administrative touch keyboard
* \author Norman Feske
* \date 2023-03-17
*/
/*
* 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 _RUNTIME__TOUCH_KEYBOARD_H_
#define _RUNTIME__TOUCH_KEYBOARD_H_
#include <runtime.h>
namespace Sculpt {
enum class Alpha { OPAQUE, ALPHA };
struct Touch_keyboard_attr
{
unsigned min_width, min_height;
Alpha alpha;
Color background;
};
static inline void gen_touch_keyboard(Xml_generator &, Touch_keyboard_attr);
}
void Sculpt::gen_touch_keyboard(Xml_generator &xml, Touch_keyboard_attr const attr)
{
xml.node("start", [&] () {
gen_common_start_content(xml, "manager_keyboard",
Cap_quota{700}, Ram_quota{18*1024*1024},
Priority::LEITZENTRALE);
gen_named_node(xml, "binary", "touch_keyboard", [&] () { });
xml.node("config", [&] () {
xml.attribute("min_width", attr.min_width);
xml.attribute("min_height", attr.min_height);
if (attr.alpha == Alpha::OPAQUE)
xml.attribute("opaque", "yes");
xml.attribute("background", String<20>(attr.background));
});
xml.node("route", [&] () {
gen_parent_rom_route(xml, "ld.lib.so");
gen_parent_rom_route(xml, "touch_keyboard");
gen_parent_rom_route(xml, "layout", "touch_keyboard_layout.config");
gen_parent_rom_route(xml, "menu_view");
gen_parent_rom_route(xml, "ld.lib.so");
gen_parent_rom_route(xml, "vfs.lib.so");
gen_parent_rom_route(xml, "libc.lib.so");
gen_parent_rom_route(xml, "libm.lib.so");
gen_parent_rom_route(xml, "libpng.lib.so");
gen_parent_rom_route(xml, "zlib.lib.so");
gen_parent_rom_route(xml, "sandbox.lib.so");
gen_parent_rom_route(xml, "menu_view_styles.tar");
gen_parent_route<Cpu_session> (xml);
gen_parent_route<Pd_session> (xml);
gen_parent_route<Log_session> (xml);
gen_parent_route<Timer::Session> (xml);
gen_service_node<::File_system::Session>(xml, [&] () {
xml.attribute("label", "fonts");
xml.node("parent", [&] () {
xml.attribute("label", "leitzentrale -> fonts"); }); });
gen_service_node<Gui::Session>(xml, [&] () {
xml.node("parent", [&] () {
xml.attribute("label", "leitzentrale -> touch_keyboard"); }); });
gen_service_node<Event::Session>(xml, [&] () {
xml.node("parent", [&] () {
xml.attribute("label", "global"); }); });
});
});
}
#endif /* _RUNTIME__TOUCH_KEYBOARD_H_ */

View File

@ -0,0 +1,15 @@
TARGET := phone_manager
SCULPT_MANAGER_DIR := $(call select_from_repositories,src/app/sculpt_manager)
DEPOT_DEPLOY_DIR := $(call select_from_repositories,src/app/depot_deploy)
SRC_CC += $(notdir $(wildcard $(PRG_DIR)/*.cc))
SRC_CC += $(addprefix view/, $(notdir $(wildcard $(SCULPT_MANAGER_DIR)/view/*.cc)))
SRC_CC += $(addprefix runtime/, $(notdir $(wildcard $(SCULPT_MANAGER_DIR)/runtime/*.cc)))
SRC_CC += $(addprefix dialog/, $(notdir $(wildcard $(SCULPT_MANAGER_DIR)/dialog/*.cc)))
SRC_CC += gui.cc graph.cc deploy.cc storage.cc network.cc
LIBS += base
INC_DIR += $(PRG_DIR) $(SCULPT_MANAGER_DIR) $(DEPOT_DEPLOY_DIR)
vpath %.cc $(PRG_DIR)
vpath %.cc $(SCULPT_MANAGER_DIR)

View File

@ -0,0 +1,250 @@
/*
* \brief Widget for configuring a new component deployed from a depot package
* \author Norman Feske
* \date 2023-03-23
*/
/*
* 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 _VIEW__COMPONENT_ADD_WIDGET_H_
#define _VIEW__COMPONENT_ADD_WIDGET_H_
#include <view/index_menu_widget.h>
#include <view/pd_route_widget.h>
#include <view/resource_widget.h>
#include <view/debug_widget.h>
#include <view/pd_route_widget.h>
#include <view/component_info_widget.h>
namespace Sculpt { struct Component_add_widget; }
struct Sculpt::Component_add_widget : Widget<Vbox>
{
using Name = Start_name;
using Action = Component::Construction_action;
Runtime_config const &_runtime_config;
using Route_entry = Hosted<Vbox, Frame, Vbox, Menu_entry>;
using Service_entry = Hosted<Vbox, Frame, Vbox, Menu_entry>;
Hosted<Vbox, Index_menu_widget::Sub_menu_title> _back { Id { "back" } };
Hosted<Vbox, Deferred_action_button> _launch { Id { "Add component" } };
Hosted<Vbox, Frame, Vbox, Resource_widget> _resources { Id { "resources" } };
Hosted<Vbox, Frame, Pd_route_widget> _pd_route { Id { "pd_route" }, _runtime_config };
Hosted<Vbox, Frame, Debug_widget> _debug { Id { "debug" } };
Id _selected_route { };
template <typename FN>
void _apply_to_selected_route(Action &action, FN const &fn)
{
unsigned count = 0;
action.apply_to_construction([&] (Component &component) {
component.routes.for_each([&] (Route &route) {
if (_route_selected(Route::Id(count++)))
fn(route); }); });
}
bool _route_selected(Route::Id const &id) const
{
return _selected_route.valid() && id == _selected_route.value;
}
bool _resource_widget_selected() const
{
return _route_selected("resources");
}
void _view_pkg_elements(Scope<Vbox> &s, Component const &component) const
{
using Info = Component::Info;
s.widget(_back, Name { "Add ", Pretty(component.name) });
s.widget(Hosted<Vbox, Component_info_widget> { Id { "info" } }, component);
s.sub_scope<Annotation>(Info(Capacity{component.ram}, " ",
component.caps, " caps"));
s.sub_scope<Vgap>();
unsigned count = 0;
component.routes.for_each([&] (Route const &route) {
Id const id { Id::Value { count++ } };
s.sub_scope<Frame>([&] (Scope<Vbox, Frame> &s) {
s.sub_scope<Vbox>([&] (Scope<Vbox, Frame, Vbox> &s) {
bool const selected = _route_selected(id.value);
bool const defined = route.selected_service.constructed();
if (!selected) {
Route_entry entry { id };
s.widget(entry, defined,
defined ? Info(route.selected_service->info)
: Info(route));
}
/*
* List of routing options
*/
if (selected) {
Route_entry back { Id { "back" } };
s.widget(back, true, Info(route), "back");
unsigned count = 0;
_runtime_config.for_each_service([&] (Service const &service) {
Id const service_id { Id::Value("service.", count++) };
bool const service_selected =
route.selected_service.constructed() &&
service_id.value == route.selected_service_id;
if (service.type == route.required) {
Service_entry entry { service_id };
s.widget(entry, service_selected, service.info);
}
});
}
});
});
});
/* don't show the PD menu if only the system PD service is available */
if (_runtime_config.num_service_options(Service::Type::PD) > 1)
s.sub_scope<Frame>([&] (Scope<Vbox, Frame> &s) {
s.widget(_pd_route, _selected_route, component); });
s.sub_scope<Frame>(Id { "resources" }, [&] (Scope<Vbox, Frame> &s) {
s.sub_scope<Vbox>([&] (Scope<Vbox, Frame, Vbox> &s) {
bool const selected = _route_selected("resources");
if (!selected) {
Route_entry entry { Id { "resources" } };
s.widget(entry, false, "Resource assignment ...", "enter");
}
if (selected) {
Route_entry entry { Id { "back" } };
s.widget(entry, true, "Resource assignment ...", "back");
s.widget(_resources, component);
}
});
});
s.sub_scope<Frame>([&] (Scope<Vbox, Frame> &s) {
s.widget(_debug, component); });
/*
* Display "Add component" button once all routes are defined
*/
if (component.all_routes_defined())
s.widget(_launch);
}
Component_add_widget(Runtime_config const &runtime_config)
:
_runtime_config(runtime_config)
{ }
void view(Scope<Vbox> &s, Component const &component) const
{
_view_pkg_elements(s, component);
}
void click(Clicked_at const &at, Action &action, auto const &leave_fn)
{
_back.propagate(at, [&] { leave_fn(); });
_launch.propagate(at);
Id const route_id = at.matching_id<Vbox, Frame, Vbox, Menu_entry>();
/* select route to present routing options */
if (!_selected_route.valid() && route_id.valid())
_selected_route = route_id;
/*
* Route selected
*/
/* close selected route */
if (route_id.value == "back") {
_selected_route = { };
} else if (_resource_widget_selected()) {
bool const clicked_on_different_route = route_id.valid();
if (clicked_on_different_route) {
_selected_route = route_id;
} else {
action.apply_to_construction([&] (Component &component) {
_resources.propagate(at, component); });
}
} else {
bool clicked_on_selected_route = false;
_apply_to_selected_route(action, [&] (Route &route) {
unsigned count = 0;
_runtime_config.for_each_service([&] (Service const &service) {
Id const id { Id::Value("service.", count++) };
if (route_id == id) {
bool const clicked_service_already_selected =
route.selected_service.constructed() &&
id.value == route.selected_service_id;
if (clicked_service_already_selected) {
/* clear selection */
route.selected_service.destruct();
route.selected_service_id = { };
} else {
/* select different service */
route.selected_service.construct(service);
route.selected_service_id = id.value;
}
_selected_route = { };
clicked_on_selected_route = true;
}
});
});
if (_selected_route == _pd_route.id)
action.apply_to_construction([&] (Component &component) {
_pd_route.propagate(at, component); });
/* select different route */
if (!clicked_on_selected_route && route_id.valid())
_selected_route = route_id;
}
}
void clack(Clacked_at const &at, auto const &launch_fn)
{
_launch.propagate(at, launch_fn);
}
};
#endif /* _VIEW__COMPONENT_ADD_WIDGET_H_ */

View File

@ -0,0 +1,34 @@
/*
* \brief Widget for displaying component information for the add menu
* \author Norman Feske
* \date 2023-11-13
*/
/*
* 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 _VIEW__COMPONENT_INFO_WIDGET_H_
#define _VIEW__COMPONENT_INFO_WIDGET_H_
#include <model/component.h>
namespace Sculpt { struct Component_info_widget; }
struct Sculpt::Component_info_widget : Widget<Vbox>
{
void view(Scope<Vbox> &s, Component const &component) const
{
if (component.info.length() > 1) {
s.sub_scope<Label>(Component::Info(" ", component.info, " "));
s.sub_scope<Small_vgap>();
}
s.sub_scope<Annotation>(component.path);
}
};
#endif /* _VIEW__COMPONENT_INFO_WIDGET_H_ */

View File

@ -0,0 +1,83 @@
/*
* \brief Conditionally visible widget
* \author Norman Feske
* \date 2022-05-20
*
* Float widget that generates its content only if a given condition is true.
*/
/*
* Copyright (C) 2022 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 _VIEW__CONDITIONAL_FLOAT_WIDGET_H_
#define _VIEW__CONDITIONAL_FLOAT_WIDGET_H_
#include <view/dialog.h>
namespace Sculpt { template <typename> struct Conditional_float_widget; }
template <typename WIDGET>
struct Sculpt::Conditional_float_widget : Widget<Float>
{
Id const id;
bool const _centered;
Hosted<Float, WIDGET> hosted;
void view(Scope<Float> &s, bool condition, auto &&... args) const
{
if (!_centered) {
s.attribute("east", "yes");
s.attribute("west", "yes");
}
if (condition)
s.widget(hosted, args...);
}
void click(Clicked_at const &at, auto &&... args) { hosted.propagate(at, args...); }
void click(Clicked_at const &at, auto &&... args) const { hosted.propagate(at, args...); }
void clack(Clacked_at const &at, auto &&... args) { hosted.propagate(at, args...); }
void clack(Clacked_at const &at, auto &&... args) const { hosted.propagate(at, args...); }
void drag (Dragged_at const &at, auto &&... args) { hosted.propagate(at, args...); }
void drag (Dragged_at const &at, auto &&... args) const { hosted.propagate(at, args...); }
struct Attr { bool centered; };
Conditional_float_widget(Attr const &attr, Id const &id, auto &&... args)
:
id(id), _centered(attr.centered), hosted(id, args...)
{ }
};
namespace Sculpt {
template <typename WIDGET>
struct Conditional_widget : Hosted<Vbox, Conditional_float_widget<WIDGET> >
{
using Attr = Conditional_float_widget<WIDGET>::Attr;
Conditional_widget(Attr const &attr, Id const &id, auto &&... args)
:
/*
* The first 'id' argument is held at 'Hosted', the second 'id'
* argument is held by 'Conditional_float_widget'. The 'args' are
* forwarded to 'WIDGET'.
*/
Hosted<Vbox, Conditional_float_widget<WIDGET> >(id, attr, id, args...)
{ }
Conditional_widget(Id const &id, auto &&... args)
:
Conditional_widget(Attr { .centered = false }, id, args...)
{ }
};
}
#endif /* _VIEW__CONDITIONAL_FLOAT_WIDGET_H_ */

View File

@ -0,0 +1,165 @@
/*
* \brief Widget for interacting with the current voice call
* \author Norman Feske
* \date 2022-06-30
*/
/*
* Copyright (C) 2022-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 _VIEW__CURRENT_CALL_WIDGET_H_
#define _VIEW__CURRENT_CALL_WIDGET_H_
#include <view/dialog.h>
#include <model/current_call.h>
namespace Sculpt { struct Current_call_widget; }
struct Sculpt::Current_call_widget : Widget<Frame>
{
struct Action : Interface
{
using Number = String<128>;
virtual void accept_incoming_call() = 0;
virtual void reject_incoming_call() = 0;
virtual void hang_up() = 0;
virtual void toggle_speaker() = 0;
virtual void initiate_call() = 0;
virtual void cancel_initiated_call() = 0;
virtual void remove_last_dial_digit() = 0;
};
struct Active_call : Widget<Hbox>
{
using State = Current_call::State;
Hosted<Hbox, Right_floating_hbox, Action_button>
_accept { Id { " Accept " } },
_reject { Id { " Reject " } },
_cancel { Id { " Cancel " } },
_hang_up { Id { " Hang up " } };
Hosted<Hbox, Right_floating_hbox, Toggle_button>
_speaker { Id { " Speaker " } };
void view(Scope<Hbox> &s, Current_call const &call) const
{
auto message = [] (Current_call::State state)
{
switch (state) {
case State::NONE: break;
case State::INCOMING: return "Call from";
case State::ACCEPTED: return "Connected from";
case State::REJECTED: return "Disconnecting from";
case State::HUNG_UP: return "Disconnected from";
case State::INITIATED: return "Dialing";
case State::OUTBOUND: return "Connecting to";
case State::ALERTING: return "Alerting";
case State::ACTIVE: return "Connected to";
case State::CANCELED: return "Canceled call to";
}
return "Failed"; /* never reached */
};
auto padded = [] (auto const &s) { return String<128>(" ", s, " "); };
s.sub_scope<Vbox>([&] (Scope<Hbox, Vbox> &s) {
s.sub_scope<Min_ex>(15);
s.sub_scope<Label>(padded(message(call.state)));
s.sub_scope<Label>(padded(call.number));
});
s.sub_scope<Right_floating_hbox>([&] (Scope<Hbox, Right_floating_hbox> &s) {
if (call.incoming()) {
s.widget(_accept);
s.widget(_reject);
}
if (call.connecting())
s.widget(_cancel);
if (call.accepted() || call.active()) {
s.widget(_speaker, call.speaker);
s.widget(_hang_up);
}
});
}
void click(Clicked_at const &at, Action &action)
{
_reject .propagate(at, [&] { action.reject_incoming_call(); });
_accept .propagate(at, [&] { action.accept_incoming_call(); });
_speaker.propagate(at, [&] { action.toggle_speaker(); });
_hang_up.propagate(at, [&] { action.hang_up(); });
_cancel .propagate(at, [&] { action.cancel_initiated_call(); });
}
};
struct Operations : Widget<Hbox>
{
struct Call_button : Action_button
{
void view(Scope<Button> &s, bool ready, auto text) const
{
Action_button::view(s, [&] (Scope<Button> &s) {
if (!ready)
s.attribute("style", "unimportant");
s.sub_scope<Label>(text);
});
}
};
Hosted<Hbox, Float, Hbox, Call_button> _clear { Id { "clear" } },
_initiate { Id { "initiate" } };;
void view(Scope<Hbox> &s, Dialed_number const &number) const
{
s.sub_scope<Vbox>([&] (Scope<Hbox, Vbox> &s) {
s.sub_scope<Vgap>();
s.sub_scope<Vgap>(); });
s.sub_scope<Float>([&] (Scope<Hbox, Float> &s) {
s.sub_scope<Hbox>([&] (Scope<Hbox, Float, Hbox> &s) {
s.widget(_clear, number.at_least_one_digit(), " Clear ");
s.sub_scope<Min_ex>(2);
s.widget(_initiate, number.suitable_for_call(), " Initiate ");
});
});
s.sub_scope<Button_vgap>();
}
void click(Clicked_at const &at, Action &action)
{
_clear .propagate(at, [&] { action.remove_last_dial_digit(); });
_initiate.propagate(at, [&] { action.initiate_call(); });
};
};
Hosted<Frame, Active_call> _active_call { Id { "active_call" } };
Hosted<Frame, Operations> _operations { Id { "operations" } };
void view(Scope<Frame> &s, Dialed_number const &number, Current_call const &call) const
{
if (call.none()) {
s.attribute("style", "invisible");
s.widget(_operations, number);
} else {
s.attribute("style", "transient");
s.widget(_active_call, call);
}
}
void click(Clicked_at const &at, Action &action)
{
_active_call.propagate(at, action);
_operations .propagate(at, action);
}
};
#endif /* _VIEW__CURRENT_CALL_WIDGET_H_ */

View File

@ -0,0 +1,148 @@
/*
* \brief Device controls (e.g., brightness) widget
* \author Norman Feske
* \date 2022-11-22
*/
/*
* Copyright (C) 2022 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 _VIEW__DEVICE_CONTROLS_WIDGET_H_
#define _VIEW__DEVICE_CONTROLS_WIDGET_H_
#include <view/dialog.h>
#include <model/power_state.h>
#include <model/mic_state.h>
#include <model/audio_volume.h>
namespace Sculpt { struct Device_controls_widget; }
struct Sculpt::Device_controls_widget : Widget<Vbox>
{
struct Level : Widget<Frame>
{
struct Bar : Widget<Right_floating_hbox>
{
void view(Scope<Right_floating_hbox> &s, unsigned const percent) const
{
for (unsigned i = 0; i < 10; i++) {
s.sub_scope<Button>(Id { i }, [&] (Scope<Right_floating_hbox, Button> &s) {
if (s.hovered()) s.attribute("hovered", "yes");
if (i*10 <= percent)
s.attribute("selected", "yes");
else
s.attribute("style", "unimportant");
s.sub_scope<Label>(" ");
});
}
}
void click(Clicked_at const &at, auto const &fn)
{
Id const id = at.matching_id<Right_floating_hbox, Button>();
unsigned value = 0;
if (!ascii_to(id.value.string(), value))
return;
unsigned const percent = max(10u, min(100u, value*10 + 9));
fn(percent);
}
};
Hosted<Frame, Bar> _bar { Id { "bar" } };
void view(Scope<Frame> &s, unsigned const percent) const
{
s.attribute("style", "important");
s.sub_scope<Left_floating_text>(s.id.value);
s.widget(_bar, percent);
}
void click(Clicked_at const &at, auto const &fn) { _bar.propagate(at, fn); }
};
Hosted<Vbox, Level> _brightness { Id { "Brightness" } },
_volume { Id { "Volume" } };
struct Mic_choice : Widget<Frame>
{
using Mic_button = Hosted<Frame, Right_floating_hbox, Select_button<Mic_state> >;
Mic_button _off { Id { " Off " }, Mic_state::OFF },
_phone { Id { " Phone " }, Mic_state::PHONE },
_on { Id { " On " }, Mic_state::ON };
void view(Scope<Frame> &s, Mic_state state) const
{
s.attribute("style", "important");
s.sub_scope<Left_floating_text>(s.id.value);
s.sub_scope<Right_floating_hbox>([&] (Scope<Frame, Right_floating_hbox> &s) {
s.widget(_off, state);
s.widget(_phone, state);
s.widget(_on, state);
});
}
void click(Clicked_at const &at, auto const &fn)
{
_off .propagate(at, [&] (Mic_state s) { fn(s); });
_phone.propagate(at, [&] (Mic_state s) { fn(s); });
_on .propagate(at, [&] (Mic_state s) { fn(s); });
}
};
Hosted<Vbox, Mic_choice> _mic_choice { Id { "Microphone" } };
void view(Scope<Vbox> &s,
Power_state const &power,
Mic_state const &mic,
Audio_volume const &audio) const
{
s.widget(_brightness, power.brightness);
s.sub_scope<Vgap>();
s.widget(_volume, audio.value);
s.sub_scope<Vgap>();
s.widget(_mic_choice, mic);
}
struct Action : Interface
{
virtual void select_brightness_level(unsigned) = 0;
virtual void select_volume_level(unsigned) = 0;
virtual void select_mic_policy(Mic_state const &) = 0;
};
void _click_or_drag(Clicked_at const &at, Action &action)
{
_brightness.propagate(at, [&] (unsigned percent) {
action.select_brightness_level(percent); });
_volume.propagate(at, [&] (unsigned percent) {
action.select_volume_level(percent); });
}
void click(Clicked_at const &at, Action &action)
{
_click_or_drag(at, action);
_mic_choice.propagate(at, [&] (Mic_state policy) {
action.select_mic_policy(policy); });
}
void drag(Dragged_at const &at, Action &action)
{
_click_or_drag(clicked_at(at), action);
}
};
#endif /* _VIEW__DEVICE_CONTROLS_WIDGET_H_ */

View File

@ -0,0 +1,210 @@
/*
* \brief Device power-control widget
* \author Norman Feske
* \date 2022-11-18
*/
/*
* Copyright (C) 2022 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 _VIEW__DEVICE_POWER_WIDGET_H_
#define _VIEW__DEVICE_POWER_WIDGET_H_
#include <util/formatted_output.h>
#include <view/dialog.h>
#include <model/power_state.h>
namespace Sculpt { struct Device_power_widget; }
struct Sculpt::Device_power_widget : Widget<Vbox>
{
enum class Option { UNKNOWN, PERFORMANCE, ECONOMIC, REBOOT, OFF };
Option _selected_option { Option::UNKNOWN };
struct Conditional_confirm : Widget<Right_floating_hbox>
{
Hosted<Right_floating_hbox, Deferred_action_button> _button { Id { } };
void view(Scope<Right_floating_hbox> &s, bool condition) const
{
s.widget(_button, [&] (Scope<Button> &s) {
if (!condition)
s.attribute("style", "invisible");
s.sub_scope<Label>("Confirm", [&] (auto &s) {
if (!condition) s.attribute("style", "invisible"); });
});
}
void click(Clicked_at const &at) { _button.propagate(at); }
void clack(Clacked_at const &at, auto const &confirmed_fn)
{
_button.propagate(at, confirmed_fn);
}
};
struct Power_options : Widget<Float>
{
struct Entry : Widget<Hbox>
{
struct Attr { bool need_confirm; };
Hosted<Hbox, Radio_select_button<Option>> _radio;
Hosted<Hbox, Conditional_confirm> _confirm { Id { "confirm" } };
Entry(Option const option) : _radio(Id { "radio" }, option) { }
void view(Scope<Hbox> &s, Option const &selected, Attr attr) const
{
s.widget(_radio, selected, s.id.value);
s.widget(_confirm, attr.need_confirm && (selected == _radio.value));
}
void click(Clicked_at const &at, Option const &selected, auto const &fn)
{
_radio.propagate(at, fn);
if (selected == _radio.value)
_confirm.propagate(at);
}
void clack(Clacked_at const &at, auto const &fn)
{
_confirm.propagate(at, [&] { fn(_radio.value); });
}
};
Hosted<Float, Frame, Vbox, Entry>
_performance { Id { "Performance" }, Option::PERFORMANCE },
_economic { Id { "Economic" }, Option::ECONOMIC },
_reboot { Id { "Reboot" }, Option::REBOOT },
_off { Id { "Power down" }, Option::OFF };
void view(Scope<Float> &s, Option const &selected) const
{
s.sub_scope<Frame>([&] (Scope<Float, Frame> &s) {
s.sub_scope<Vbox>([&] (Scope<Float, Frame, Vbox> &s) {
s.widget(_performance, selected, Entry::Attr { .need_confirm = false });
s.widget(_economic, selected, Entry::Attr { .need_confirm = false });
s.widget(_reboot, selected, Entry::Attr { .need_confirm = true });
s.widget(_off, selected, Entry::Attr { .need_confirm = true });
s.sub_scope<Min_ex>(35);
});
});
}
void click(Clicked_at const &at, Option const &selected, auto const &fn)
{
_performance.propagate(at, selected, fn);
_economic .propagate(at, selected, fn);
_reboot .propagate(at, selected, fn);
_off .propagate(at, selected, fn);
}
void clack(Clacked_at const &at, auto const &fn)
{
_reboot.propagate(at, fn);
_off .propagate(at, fn);
}
};
static void _view_battery_value(Scope<> &s, auto const &label, double value, auto const &unit)
{
s.sub_scope<Hbox>([&] (Scope<Hbox> &s) {
s.sub_scope<Label>(label, [&] (Scope<Hbox, Label> &s) {
s.attribute("min_ex", 23); });
auto pretty_value = [] (double value, auto unit)
{
using Text = String<64>;
if (value < 1.0)
return Text((unsigned)((value + 0.0005)*1000), " m", unit);
unsigned const decimal = (unsigned)(value + 0.005);
unsigned const hundredth = (unsigned)(100*(value - decimal));
return Text(decimal, ".",
Repeated(2 - printed_length(hundredth), "0"),
hundredth, " ", unit);
};
s.sub_scope<Label>(pretty_value(value, unit), [&] (Scope<Hbox, Label> &s) {
s.attribute("min_ex", 8); });
});
}
Hosted<Vbox, Power_options> _power_options { Id { "options" } };
void view(Scope<Vbox> &s, Power_state const &power_state) const
{
auto curr_selection = [&]
{
if (_selected_option == Option::UNKNOWN) {
if (power_state.profile == Power_state::Profile::PERFORMANCE)
return Option::PERFORMANCE;
if (power_state.profile == Power_state::Profile::ECONOMIC)
return Option::ECONOMIC;
}
return _selected_option;
};
s.widget(_power_options, curr_selection());
if (power_state.battery_present) {
s.sub_scope<Centered_info_vbox>([&] (Scope<Vbox, Centered_info_vbox> &s) {
s.as_new_scope([&] (Scope<> &s) {
if (power_state.charging)
_view_battery_value(s, " Battery charge current ",
power_state.battery.charge_current, "A");
else
_view_battery_value(s, " Battery power draw ",
power_state.battery.power_draw, "W");
});
s.sub_scope<Min_ex>(35);
});
}
}
struct Action : Interface
{
virtual void activate_performance_power_profile() = 0;
virtual void activate_economic_power_profile() = 0;
virtual void trigger_device_reboot() = 0;
virtual void trigger_device_off() = 0;
};
void click(Clicked_at const &at, Action &action)
{
_power_options.propagate(at, _selected_option, [&] (Option const selected) {
_selected_option = selected;
if (selected == Option::PERFORMANCE) action.activate_performance_power_profile();
if (selected == Option::ECONOMIC) action.activate_economic_power_profile();
});
}
void clack(Clacked_at const &at, Action &action)
{
_power_options.propagate(at, [&] (Option const confirmed) {
if (confirmed == Option::REBOOT) action.trigger_device_reboot();
if (confirmed == Option::OFF) action.trigger_device_off();
});
}
};
#endif /* _VIEW__DEVICE_POWER_WIDGET_H_ */

View File

@ -0,0 +1,86 @@
/*
* \brief Dialpad widget
* \author Norman Feske
* \date 2022-06-29
*/
/*
* Copyright (C) 2022 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 _VIEW__DIALPAD_WIDGET_H_
#define _VIEW__DIALPAD_WIDGET_H_
#include <view/dialog.h>
#include <model/dialed_number.h>
namespace Sculpt { struct Dialpad_widget; }
struct Sculpt::Dialpad_widget : Widget<Centered_dialog_vbox>
{
Hosted<Centered_dialog_vbox, Pin_row>
_rows[4] { { Id { "r1" }, "1", "2", "3" },
{ Id { "r2" }, "4", "5", "6" },
{ Id { "r3" }, "7", "8", "9" },
{ Id { "r4" }, "*", "0", "#" } };
void view(Scope<Centered_dialog_vbox> &s, Dialed_number const &dialed_number) const
{
using Text = String<64>;
Text digits(dialed_number);
/* if number grows too large, show the tail end */
if (digits.length() > 28)
digits = Text("...", Cstring(digits.string() + digits.length() - 27));
s.sub_scope<Min_ex>(20);
s.sub_scope<Vgap>();
s.sub_scope<Button>([&] (Scope<Centered_dialog_vbox, Button> &s) {
s.attribute("style", "invisible");
s.sub_scope<Float>([&] (Scope<Centered_dialog_vbox, Button, Float> &s) {
s.attribute("west", "yes");
s.sub_scope<Label>(" Dial", [&] (auto &s) {
s.attribute("font", "title/regular");
if (digits.length() > 12)
s.attribute("style", "invisible");
});
});
s.sub_scope<Hbox>([&] (Scope<Centered_dialog_vbox, Button, Hbox> &s) {
if (digits.length() <= 12)
s.sub_scope<Min_ex>(16);
s.sub_scope<Float>([&] (Scope<Centered_dialog_vbox, Button, Hbox, Float> &s) {
s.sub_scope<Label>(Text(digits), [&] (auto &s) {
s.attribute("min_ex", 15);
if (digits.length() < 20)
s.attribute("font", "title/regular"); });
s.sub_scope<Label>(" ", [&] (auto &s) {
s.attribute("font", "title/regular"); });
});
});
});
s.sub_scope<Vgap>();
for (auto const &row : _rows)
s.widget(row);
}
struct Action : Interface
{
virtual void append_dial_digit(Dialed_number::Digit) = 0;
};
void click(Clicked_at const &at, Action &action)
{
for (auto &row : _rows)
row.propagate(at, [&] (auto const &label) {
Dialed_number::Digit const digit { label.string()[0] };
action.append_dial_digit(digit); });
}
};
#endif /* _VIEW__DIALPAD_WIDGET_H_ */

View File

@ -0,0 +1,178 @@
/*
* \brief Widget for browsing a depot index
* \author Norman Feske
* \date 2023-03-21
*/
/*
* 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 _VIEW__INDEX_MENU_WIDGET_H_
#define _VIEW__INDEX_MENU_WIDGET_H_
#include <view/dialog.h>
#include <model/index_menu.h>
namespace Sculpt { struct Index_menu_widget; }
struct Sculpt::Index_menu_widget : Widget<Vbox>
{
public:
using User = Depot::Archive::User;
using Index = Attached_rom_dataspace;
using Name = Start_name;
struct Sub_menu_title : Widget<Left_floating_hbox>
{
void view(Scope<Left_floating_hbox> &s, auto const &text) const
{
bool const hovered = (s.hovered() && !s.dragged());
s.sub_scope<Icon>("back", Icon::Attr { .hovered = hovered,
.selected = true });
s.sub_scope<Label>(" ");
s.sub_scope<Label>(text, [&] (auto &s) {
s.attribute("font", "title/regular"); });
/* inflate vertical space to button size */
s.sub_scope<Button>([&] (Scope<Left_floating_hbox, Button> &s) {
s.attribute("style", "invisible");
s.sub_scope<Label>(""); });
}
void click(Clicked_at const &, auto const &fn) const { fn(); }
};
private:
Index const &_index;
Index_menu _menu { };
bool _pkg_selected = false;
Hosted<Vbox, Sub_menu_title> const _back { Id { "back" } };
void _reset_selection() { _pkg_selected = false; }
public:
Index_menu_widget(Index const &index) : _index(index) { }
void view(Scope<Vbox> &s, User const &user, auto const &view_item_fn) const
{
if (_menu.level())
s.widget(_back, Name { _menu });
unsigned count = 0;
_menu.for_each_item(_index.xml(), user, [&] (Xml_node const &item) {
Id const id { { count } };
if (item.has_type("index")) {
auto const name = item.attribute_value("name", Name());
view_item_fn(s, id, Name { name, " ..." }, "");
}
if (item.has_type("pkg")) {
auto const path = item.attribute_value("path", Depot::Archive::Path());
auto const name = Depot::Archive::name(path);
view_item_fn(s, id, name, path);
}
count++;
});
}
void click(Clicked_at const &at, User const &user,
auto const &enter_pkg_fn,
auto const &leave_pkg_fn,
auto const &pkg_operation_fn)
{
/* go one menu up */
_back.propagate(at, [&] {
_menu._selected[_menu._level] = Index_menu::Name();
_menu._level--;
_pkg_selected = false;
leave_pkg_fn();
});
/* enter sub menu of index */
if (_menu._level < Index_menu::MAX_LEVELS - 1) {
Id const clicked = at.matching_id<Vbox, Menu_entry>();
unsigned count = 0;
_menu.for_each_item(_index.xml(), user, [&] (Xml_node const &item) {
if (clicked == Id { { count } }) {
if (item.has_type("index")) {
Index_menu::Name const name =
item.attribute_value("name", Index_menu::Name());
_menu._selected[_menu._level] = name;
_menu._level++;
} else if (item.has_type("pkg")) {
_pkg_selected = true;
enter_pkg_fn(item);
}
}
count++;
});
if (at.matching_id<Vbox, Float>() == Id { "pkg" })
pkg_operation_fn(at);
}
}
void clack(Clacked_at const &at, auto const &pkg_operation_fn)
{
if (at.matching_id<Vbox, Float>() == Id { "pkg" })
pkg_operation_fn(at);
}
bool top_level() const { return (_menu.level() == 0) && !_pkg_selected; }
bool pkg_selected() const { return _pkg_selected; }
void deselect_pkg() { _pkg_selected = false; }
void reset()
{
_menu = Index_menu { };
_reset_selection();
}
void one_level_back()
{
if (_menu.level() == 0)
_menu._level--;
_reset_selection();
}
bool anything_visible(User const &user) const
{
if (_menu.level())
return true;
bool at_least_one_item_exists = false;
_menu.for_each_item(_index.xml(), user, [&] (Xml_node const &) {
at_least_one_item_exists = true; });
return at_least_one_item_exists;
}
};
#endif /* _VIEW__INDEX_MENU_WIDGET_H_ */

View File

@ -0,0 +1,106 @@
/*
* \brief Widget for presenting pkg details and the option for installation
* \author Norman Feske
* \date 2023-03-22
*/
/*
* 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 _VIEW__INDEX_PKG_WIDGET_H_
#define _VIEW__INDEX_PKG_WIDGET_H_
#include <model/nic_state.h>
#include <view/depot_users_widget.h>
#include <view/component_info_widget.h>
namespace Sculpt { struct Index_pkg_widget; }
struct Sculpt::Index_pkg_widget : Widget<Float>
{
using User_properties = Depot_users_widget::User_properties;
Hosted<Float, Vbox, Float, Deferred_action_button> _install { Id { "install" } };
void view(Scope<Float> &s, Component const &component,
User_properties const &properties, Nic_state const &nic_state) const
{
if (!component.blueprint_info.known || component.blueprint_info.ready_to_deploy())
return;
s.sub_scope<Vbox>([&] (Scope<Float, Vbox> &s) {
/*
* Package is installed but content is missing
*
* This can happen when the pkg's runtime is inconsistent with
* the content contained in the pkg's archives.
*/
if (component.blueprint_info.incomplete()) {
s.sub_scope<Small_vgap>();
s.sub_scope<Annotation>(component.path);
s.sub_scope<Small_vgap>();
s.sub_scope<Label>("installed but incomplete");
if (nic_state.ready()) {
s.sub_scope<Small_vgap>();
auto const text = properties.public_key
? " Reattempt Install "
: " Reattempt Install without Verification ";
s.sub_scope<Float>([&] (Scope<Float, Vbox, Float> &s) {
s.widget(_install, [&] (Scope<Button> &s) {
s.sub_scope<Label>(text); }); });
}
s.sub_scope<Small_vgap>();
}
/*
* Package is missing but can be installed
*/
else if (component.blueprint_info.uninstalled() && nic_state.ready()) {
s.widget(Hosted<Float, Vbox, Component_info_widget> { Id { "info" } }, component);
auto const text = properties.public_key
? " Install "
: " Install without Verification ";
s.sub_scope<Vgap>();
s.sub_scope<Float>([&] (Scope<Float, Vbox, Float> &s) {
s.widget(_install, [&] (Scope<Button> &s) {
s.sub_scope<Label>(text); }); });
s.sub_scope<Vgap>();
}
/*
* Package is missing and we cannot do anything about it
*/
else if (component.blueprint_info.uninstalled()) {
s.sub_scope<Vgap>();
s.sub_scope<Annotation>(component.path);
s.sub_scope<Vgap>();
s.sub_scope<Label>("not installed");
s.sub_scope<Vgap>();
}
});
}
void click(Clicked_at const &at)
{
_install.propagate(at);
}
void clack(Clacked_at const &at, auto const install_fn)
{
_install.propagate(at, install_fn);
}
};
#endif /* _VIEW__INDEX_PKG_WIDGET_H_ */

View File

@ -0,0 +1,50 @@
/*
* \brief Modem power-control widget
* \author Norman Feske
* \date 2022-05-20
*/
/*
* Copyright (C) 2022 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 _VIEW__MODEM_POWER_WIDGET_H_
#define _VIEW__MODEM_POWER_WIDGET_H_
#include <view/dialog.h>
#include <model/modem_state.h>
namespace Sculpt { struct Modem_power_widget; }
struct Sculpt::Modem_power_widget : Widget<Frame>
{
Hosted<Frame, Right_floating_off_on> _power_switch { Id { "power" } };
void view(Scope<Frame> &s, Modem_state const &state) const
{
s.attribute("style", "important");
s.sub_scope<Left_floating_text>("Modem Power");
s.widget(_power_switch, Right_floating_off_on::Attr {
.on = state.on(),
.transient = state.transient()
});
}
struct Action : Interface
{
virtual void modem_power(bool) = 0;
};
void click(Clicked_at const &at, Action &action)
{
_power_switch.propagate(at, [&] (bool on) { action.modem_power(on); });
}
};
#endif /* _VIEW__MODEM_POWER_WIDGET_H_ */

View File

@ -0,0 +1,46 @@
/*
* \brief Widget for initiating calls
* \author Norman Feske
* \date 2022-06-29
*/
/*
* Copyright (C) 2022 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 _VIEW__OUTBOUND_WIDGET_H_
#define _VIEW__OUTBOUND_WIDGET_H_
#include <view/dialog.h>
namespace Sculpt { struct Outbound_widget; }
struct Sculpt::Outbound_widget : Widget<Frame>
{
void view(Scope<Frame> &s) const
{
s.sub_scope<Hbox>([&] (Scope<Frame, Hbox> &s) {
s.sub_scope<Button>([&] (Scope<Frame, Hbox, Button> &s) {
s.attribute("style", "unimportant");
s.sub_scope<Label>("History");
});
s.sub_scope<Button>([&] (Scope<Frame, Hbox, Button> &s) {
s.attribute("style", "unimportant");
s.sub_scope<Label>("Contacts");
});
s.sub_scope<Button>([&] (Scope<Frame, Hbox, Button> &s) {
s.attribute("selected", "yes");
s.sub_scope<Label>("Dial");
});
});
}
};
#endif /* _VIEW__OUTBOUND_WIDGET_H_ */

View File

@ -0,0 +1,88 @@
/*
* \brief SIM PIN entry widget
* \author Norman Feske
* \date 2022-05-20
*/
/*
* Copyright (C) 2022 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 _VIEW__PIN_WIDGET_H_
#define _VIEW__PIN_WIDGET_H_
#include <view/dialog.h>
#include <model/sim_pin.h>
namespace Sculpt { struct Pin_widget; }
struct Sculpt::Pin_widget : Widget<Centered_dialog_vbox>
{
Hosted<Centered_dialog_vbox, Pin_row>
_rows[3] { { Id { "r1" }, "1", "2", "3" },
{ Id { "r2" }, "4", "5", "6" },
{ Id { "r3" }, "7", "8", "9" } },
_last_row { Id { "r4" }, "C", "0", "OK" };
void view(Scope<Centered_dialog_vbox> &s, Blind_sim_pin const &sim_pin) const
{
s.sub_scope<Min_ex>(20);
s.sub_scope<Vgap>();
s.sub_scope<Hbox>([&] (Scope<Centered_dialog_vbox, Hbox> &s) {
s.sub_scope<Label>(" Enter SIM PIN ", [&] (auto &s) {
s.attribute("min_ex", 5);
s.attribute("font", "title/regular");
});
s.sub_scope<Label>(String<64>(" ", sim_pin, " "), [&] (auto &s) {
s.attribute("min_ex", 5);
s.attribute("font", "title/regular");
});
});
s.sub_scope<Vgap>();
for (auto const &row : _rows)
s.widget(row);
s.widget(_last_row,
Pin_row::Visible { .left = sim_pin.at_least_one_digit(),
.middle = true,
.right = sim_pin.suitable_for_unlock() });
}
struct Action : Interface
{
virtual void append_sim_pin_digit(Sim_pin::Digit) = 0;
virtual void remove_last_sim_pin_digit() = 0;
virtual void confirm_sim_pin() = 0;
};
void click(Clicked_at const &at, Blind_sim_pin const &sim_pin, Action &action)
{
auto for_each_row = [&] (auto const &fn)
{
for (auto &row : _rows) fn(row);
fn(_last_row);
};
for_each_row([&] (auto &row) {
row.propagate(at, [&] (auto const &label) {
for (unsigned i = 0; i < 10; i++)
if (String<10>(i) == label)
action.append_sim_pin_digit(Sim_pin::Digit{i});
if (label == "C")
action.remove_last_sim_pin_digit();
if (label == "OK" && sim_pin.suitable_for_unlock())
action.confirm_sim_pin();
});
});
}
};
#endif /* _VIEW__PIN_WIDGET_H_ */

View File

@ -0,0 +1,84 @@
/*
* \brief Section widget
* \author Norman Feske
* \date 2022-05-20
*/
/*
* Copyright (C) 2022 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 _VIEW__SELECTABLE_TITLE_BAR_H_
#define _VIEW__SELECTABLE_TITLE_BAR_H_
#include <view/dialog.h>
namespace Sculpt { template <typename> struct Selectable_title_bar; }
template <typename ENUM>
struct Sculpt::Selectable_title_bar : Widget<Vbox>
{
ENUM const &_selected_value;
ENUM const value;
bool _minimized() const { return (_selected_value != value)
&& (_selected_value != ENUM::NONE); }
Selectable_title_bar(ENUM const &selected_value, ENUM value)
: _selected_value(selected_value), value(value) { }
void view(Scope<Vbox> &s, auto const &status_fn) const
{
Id const &id = s.id;
s.sub_scope<Float>([&] (Scope<Vbox, Float> &s) {
s.attribute("east", "yes");
s.attribute("west", "yes");
bool const hovered = s.hovered() && (!s.dragged() || selected());
s.sub_scope<Button>([&] (Scope<Vbox, Float, Button> &s) {
if (selected()) s.attribute("selected", "yes");
if (hovered) s.attribute("hovered", "yes");
if (_minimized())
s.attribute("style", "unimportant");
s.sub_scope<Hbox>([&] (Scope<Vbox, Float, Button, Hbox> &s) {
s.sub_scope<Vbox>(id, [&] (Scope<Vbox, Float, Button, Hbox, Vbox> &s) {
s.sub_scope<Min_ex>(12);
s.sub_scope<Label>(id.value, [&] (auto &s) {
if (_minimized())
s.attribute("font", "annotation/regular");
else
s.attribute("font", "title/regular");
});
});
s.sub_scope<Vbox>([&] (Scope<Vbox, Float, Button, Hbox, Vbox> &s) {
s.sub_scope<Min_ex>(12);
status_fn(s); });
});
});
});
}
void click(Clicked_at const &, auto const &fn) { fn(); }
void view_status(auto &s, auto const &text) const
{
s.template sub_scope<Label>(text, [&] (auto &s) {
if (_minimized())
s.attribute("font", "annotation/regular"); });
}
bool selected() const { return _selected_value == value; }
};
#endif /* _VIEW__SECTION_DIALOG_H_ */

View File

@ -0,0 +1,271 @@
/*
* \brief Dialog for adding software components
* \author Norman Feske
* \date 2023-03-21
*/
/*
* 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 _VIEW__SOFTWARE_ADD_WIDGET_H_
#define _VIEW__SOFTWARE_ADD_WIDGET_H_
#include <model/build_info.h>
#include <model/nic_state.h>
#include <model/index_update_queue.h>
#include <model/index_menu.h>
#include <view/depot_users_widget.h>
#include <view/index_menu_widget.h>
#include <view/index_pkg_widget.h>
#include <view/component_add_widget.h>
namespace Sculpt { struct Software_add_widget; }
struct Sculpt::Software_add_widget : Widget_interface<Vbox>
{
using Depot_users = Depot_users_widget::Depot_users;
using User = Depot_users_widget::User;
using Url = Depot_users_widget::Url;
using User_properties = Depot_users_widget::User_properties;
using Index = Index_menu_widget::Index;
using Construction_info = Component::Construction_info;
Build_info const _build_info;
Sculpt_version const _sculpt_version;
Nic_state const &_nic_state;
Index_update_queue const &_index_update_queue;
Download_queue const &_download_queue;
Construction_info const &_construction_info;
Depot_users const &_depot_users;
Hosted<Vbox, Frame, Vbox, Depot_users_widget> _users;
Hosted<Vbox, Float, Frame, Vbox, Index_menu_widget> _menu;
Hosted<Vbox, Float, Frame, Vbox, Component_add_widget> _component_add;
using Hosted_pkg_widget = Hosted<Vbox, Index_pkg_widget>;
Hosted_pkg_widget _pkg;
Path _index_path() const { return Path(_users.selected(), "/index/", _sculpt_version); }
bool _index_update_in_progress() const
{
using Update = Index_update_queue::Update;
bool result = false;
_index_update_queue.with_update(_index_path(), [&] (Update const &update) {
if (update.active())
result = true; });
return result;
}
Software_add_widget(Build_info const &build_info,
Sculpt_version const &sculpt_version,
Nic_state const &nic_state,
Index_update_queue const &index_update_queue,
Index const &index,
Download_queue const &download_queue,
Runtime_config const &runtime_config,
Construction_info const &construction_info,
Depot_users const &depot_users)
:
_build_info(build_info), _sculpt_version(sculpt_version),
_nic_state(nic_state), _index_update_queue(index_update_queue),
_download_queue(download_queue), _construction_info(construction_info),
_depot_users(depot_users),
_users(Id { "users" }, depot_users, _build_info.depot_user),
_menu(Id { "menu" }, index),
_component_add(Id { "add" }, runtime_config),
_pkg(Id { "pkg" })
{ }
struct Index_menu_entry
{
Download_queue const &download_queue;
Construction_info const &construction_info;
Hosted_pkg_widget const &pkg;
Depot_users_widget const &users;
Nic_state const &nic_state;
using Hosted_entry = Hosted<Vbox, Menu_entry>;
void view(Scope<Vbox> &s, Id const &id, auto const &text, Path const &pkg_path)
{
bool pkg_selected = false;
bool pkg_installing = false;
String<100> label { text };
if (pkg_path.length() > 1) {
pkg_installing = download_queue.in_progress(pkg_path);
construction_info.with_construction([&] (Component const &component) {
if (component.path == pkg_path)
pkg_selected = true; });
label = { Pretty(label), "(", Depot::Archive::version(pkg_path), ")",
pkg_installing ? " installing... " : "... " };
}
s.widget(Hosted_entry { id }, pkg_selected, label);
if (pkg_selected && !pkg_installing)
construction_info.with_construction([&] (Component const &component) {
s.widget(pkg, component, users.selected_user_properties(),
nic_state); });
}
};
bool _component_add_widget_visible() const
{
bool result = false;
if (_menu.pkg_selected())
_construction_info.with_construction([&] (Component const &component) {
if (component.blueprint_info.ready_to_deploy())
result = true; });
return result;
}
Hosted<Vbox, Frame, Vbox, Float, Operation_button> _check { Id { "check" } };
void view(Scope<Vbox> &s) const
{
s.sub_scope<Frame>([&] (Scope<Vbox, Frame> &s) {
s.sub_scope<Vbox>([&] (Scope<Vbox, Frame, Vbox> &s) {
s.widget(_users);
User_properties const properties = _users.selected_user_properties();
bool const offer_index_update = _users.one_selected()
&& _menu.top_level()
&& _nic_state.ready()
&& properties.download_url;
if (!offer_index_update)
return;
s.sub_scope<Small_vgap>();
s.sub_scope<Float>([&] (Scope<Vbox, Frame, Vbox, Float> &s) {
s.widget(_check, _index_update_in_progress(),
properties.public_key ? "Update Index"
: "Update unverified Index");
});
s.sub_scope<Small_vgap>();
});
});
if (_users.unfolded())
return;
s.sub_scope<Vgap>();
User const user = _users.selected();
if (!_component_add_widget_visible() && !_menu.anything_visible(user))
return;
s.sub_scope<Float>([&] (Scope<Vbox, Float> &s) {
s.sub_scope<Frame>([&] (Scope<Vbox, Float, Frame> &s) {
s.sub_scope<Vbox>([&] (Scope<Vbox, Float, Frame, Vbox> &s) {
s.sub_scope<Min_ex>(35);
if (_component_add_widget_visible())
_construction_info.with_construction([&] (Component const &component) {
s.widget(_component_add, component); });
else {
s.widget(_menu, user,
[&] (Scope<Vbox> &s, auto &&... args)
{
Index_menu_entry entry {
.download_queue = _download_queue,
.construction_info = _construction_info,
.pkg = _pkg,
.users = _users,
.nic_state = _nic_state };
entry.view(s, args...);
});
}
});
});
});
}
void _reset_menu() { _menu.reset(); }
bool keyboard_needed() const { return _users.keyboard_needed(); }
struct Action : virtual Depot_users_widget::Action,
virtual Component::Construction_action
{
virtual void query_index (User const &) = 0;
virtual void update_sculpt_index(User const &, Verify) = 0;
};
void click(Clicked_at const &at, Action &action)
{
_users.propagate(at, action, [&] (User const &selected_user) {
action.query_index(selected_user);
_reset_menu();
});
Verify const verify { _users.selected_user_properties().public_key };
if (_component_add_widget_visible()) {
_component_add.propagate(at, action,
[&] /* leave */ {
action.discard_construction();
_menu.one_level_back();
}
);
} else {
_menu.propagate(at, _users.selected(),
[&] /* enter pkg */ (Xml_node const &item) {
auto path = item.attribute_value("path", Component::Path());
auto info = item.attribute_value("info", Component::Info());
action.new_construction(path, verify, info);
},
[&] /* leave pkg */ { action.discard_construction(); },
[&] /* pkg operation */ (Clicked_at const &at) {
_pkg.propagate(at); }
);
}
if (!_index_update_in_progress())
_check.propagate(at, [&] {
action.update_sculpt_index(_users.selected(), verify); });
}
void clack(Clacked_at const &at, Action &action)
{
if (_component_add_widget_visible())
_component_add.propagate(at, [&] /* launch */ {
action.launch_construction();
_reset_menu(); });
_menu.propagate(at, [&] /* pkg operation */ (Clacked_at const &at) {
_pkg.propagate(at, [&] { action.trigger_pkg_download(); }); });
}
void handle_key(Codepoint c, Action &action)
{
_users.handle_key(c, action);
}
void sanitize_user_selection() { _users.sanitize_unfold_state(); }
};
#endif /* _VIEW__SOFTWARE_ADD_WIDGET_H_ */

View File

@ -0,0 +1,89 @@
/*
* \brief Widget for the software options
* \author Norman Feske
* \date 2022-09-20
*/
/*
* Copyright (C) 2022 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 _VIEW__SOFTWARE_OPTIONS_WIDGET_H_
#define _VIEW__SOFTWARE_OPTIONS_WIDGET_H_
#include <model/launchers.h>
#include <view/dialog.h>
#include <string.h>
namespace Sculpt { struct Software_options_widget; }
struct Sculpt::Software_options_widget : Widget<Vbox>
{
Runtime_info const &_runtime_info;
Launchers const &_launchers;
Software_options_widget(Runtime_info const &runtime_info,
Launchers const &launchers)
:
_runtime_info(runtime_info), _launchers(launchers)
{ }
struct Option : Widget<Frame>
{
Hosted<Frame, Right_floating_off_on> _switch { Id { "switch" } };
void view(Scope<Frame> &s, auto const &text, bool enabled) const
{
s.attribute("style", "important");
s.sub_scope<Left_floating_text>(Pretty(text));
s.widget(_switch, enabled);
}
void click(Clicked_at const &at, auto const &fn) const
{
_switch.propagate(at, fn);
}
};
using Hosted_option = Hosted<Vbox, Option>;
void view(Scope<Vbox> &s) const
{
unsigned count = 0;
_launchers.for_each([&] (Launchers::Info const &info) {
Hosted_option option { { count++ } };
s.widget(option,
info.path, _runtime_info.present_in_runtime(info.path));
});
}
struct Action : Interface
{
virtual void enable_optional_component (Path const &launcher) = 0;
virtual void disable_optional_component(Path const &launcher) = 0;
};
void click(Clicked_at const &at, Action &action) const
{
Id const clicked_id = at.matching_id<Vbox, Option>();
unsigned count = 0;
_launchers.for_each([&] (Launchers::Info const &info) {
Id const id { { count++ } };
if (clicked_id != id)
return;
Hosted_option const option { id };
option.propagate(at, [&] (bool on) {
if (on) action.enable_optional_component(info.path);
else action.disable_optional_component(info.path);
});
});
}
};
#endif /* _VIEW__SOFTWARE_OPTIONS_WIDGET_H_ */

View File

@ -0,0 +1,89 @@
/*
* \brief Widget for the software tabs
* \author Norman Feske
* \date 2022-09-l9
*/
/*
* Copyright (C) 2022 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 _VIEW__SOFTWARE_TABS_WIDGET_H_
#define _VIEW__SOFTWARE_TABS_WIDGET_H_
#include <view/dialog.h>
#include <model/presets.h>
namespace Sculpt { struct Software_tabs_widget; }
struct Sculpt::Software_tabs_widget : Widget<Frame>
{
enum class Tab { PRESETS, RUNTIME, ADD, OPTIONS, UPDATE, STATUS };
Tab _selected = Tab::RUNTIME;
struct Tab_button : Select_button<Tab>
{
using Select_button::Select_button;
void view(Scope<Button> &s, Tab selected_value, bool ready) const
{
bool const selected = (selected_value == _value),
hovered = (s.hovered() && !s.dragged() && !selected && ready);
if (selected) s.attribute("selected", "yes");
if (hovered) s.attribute("hovered", "yes");
if (!ready) s.attribute("style", "unimportant");
s.sub_scope<Label>(s.id.value);
}
};
Hosted<Frame, Hbox, Tab_button> const
_status { Id { "Status" }, Tab::STATUS },
_presets { Id { "Presets" }, Tab::PRESETS },
_runtime { Id { "Runtime" }, Tab::RUNTIME },
_add { Id { "Add" }, Tab::ADD },
_options { Id { "Options" }, Tab::OPTIONS },
_update { Id { "Update" }, Tab::UPDATE };
void view(Scope<Frame> &s,
Storage_target const &storage_target,
Presets const &presets,
bool const status_available) const
{
s.sub_scope<Hbox>([&] (Scope<Frame, Hbox> &s) {
s.widget(_status, _selected, status_available);
s.widget(_presets, _selected, storage_target.valid() && presets.available());
s.widget(_runtime, _selected, true);
s.widget(_add, _selected, storage_target.valid());
s.widget(_options, _selected, storage_target.valid());
s.widget(_update, _selected, storage_target.valid());
});
}
void click(Clicked_at const &at, auto const &fn)
{
_status .propagate(at, [&] (Tab t) { _selected = t; });
_presets.propagate(at, [&] (Tab t) { _selected = t; });
_runtime.propagate(at, [&] (Tab t) { _selected = t; });
_add .propagate(at, [&] (Tab t) { _selected = t; });
_options.propagate(at, [&] (Tab t) { _selected = t; });
_update .propagate(at, [&] (Tab t) { _selected = t; });
fn();
}
bool presets_selected() const { return _selected == Tab::PRESETS; }
bool runtime_selected() const { return _selected == Tab::RUNTIME; }
bool options_selected() const { return _selected == Tab::OPTIONS; }
bool add_selected() const { return _selected == Tab::ADD; }
bool update_selected() const { return _selected == Tab::UPDATE; }
bool status_selected() const { return _selected == Tab::STATUS; }
};
#endif /* _VIEW__SOFTWARE_TABS_WIDGET_H_ */