diff --git a/repos/gems/src/app/menu_view/depgraph_widget.h b/repos/gems/src/app/menu_view/depgraph_widget.h
new file mode 100644
index 0000000000..db878f09ac
--- /dev/null
+++ b/repos/gems/src/app/menu_view/depgraph_widget.h
@@ -0,0 +1,665 @@
+/*
+ * \brief Widget that organizes child widgets as a directed graph
+ * \author Norman Feske
+ * \date 2017-08-09
+ */
+
+/*
+ * Copyright (C) 2017 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 _DEPGRAPH_WIDGET_H_
+#define _DEPGRAPH_WIDGET_H_
+
+/* Genode includes */
+#include
+
+/* gems includes */
+#include
+
+/* local includes */
+#include "widget.h"
+
+namespace Menu_view { struct Depgraph_widget; }
+
+
+struct Menu_view::Depgraph_widget : Widget
+{
+ Area _min_size; /* value cached from layout computation */
+
+ struct Depth_direction
+ {
+ enum Value { EAST, WEST, NORTH, SOUTH };
+ Value value;
+ bool horizontal() const { return value == EAST || value == WEST; }
+
+ } _depth_direction { Depth_direction::EAST };
+
+ struct Node
+ {
+ Allocator &_alloc;
+ Widget &_widget;
+
+ struct Anchor
+ {
+ Node &_remote;
+
+ /**
+ * Dependency type
+ *
+ * Primary dependency nodes define the topology of the tree
+ * whereas secondary dependencies are weaker links that are
+ * displayed but ignored for the topology.
+ */
+ enum Type { PRIMARY, SECONDARY } const _type;
+
+ Anchor(Node &remote, Type type) : _remote(remote), _type(type) { }
+
+ virtual ~Anchor() { }
+
+ bool primary() const { return _type == PRIMARY; }
+
+ /**
+ * Return breadth position of the anchored component
+ *
+ * The returned value is used to compute the order of anchors
+ * along the node edges.
+ */
+ int remote_centered_breadth_pos(Depth_direction dir) const
+ {
+ return _remote.centered_breadth_pos(dir);
+ }
+ };
+
+ Registry > _server_anchors;
+ Registry > _client_anchors;
+
+ struct Dependency
+ {
+ Anchor::Type const _type;
+
+ /*
+ * Dependencies are marked as out of date at the beginning of the
+ * update procedure. Each dependency visited during the update is
+ * marked as up-to-date. The remaining out-of-date dependencies
+ * are stale and must be destroyed.
+ */
+ bool up_to_date = true;
+
+ Node &_server;
+
+ /*
+ * Connection points at both ends of the dependency
+ */
+ Registered _anchor_at_server;
+ Registered _anchor_at_client;
+
+ Dependency(Node &client, Node &server, Anchor::Type type)
+ :
+ _type(type), _server(server),
+ _anchor_at_server(server._server_anchors, client, type),
+ _anchor_at_client(client._client_anchors, server, type)
+ { }
+
+ virtual ~Dependency() { }
+
+ bool depends_on(Node const &n) const { return &_server == &n; }
+
+ unsigned server_depth_pos(Depth_direction dir) const
+ {
+ return _server.depth_pos(dir) + _server.depth_size(dir);
+ }
+
+ unsigned server_breadth_pos(Depth_direction dir) const
+ {
+ return _server.breadth_pos(dir);
+ }
+
+ unsigned server_breadth_alignment(Depth_direction dir) const
+ {
+ unsigned const children_size = _server.layout_breadth_child_offset;
+ unsigned const total_size = _server.breadth_size(dir);
+
+ return children_size < total_size ? (total_size - children_size)/2 : 0;
+ }
+
+ bool primary() const { return _type == Anchor::PRIMARY; }
+
+ template
+ void apply_to_server(FN const &fn) { fn(_server); }
+
+ template
+ void apply_to_server(FN const &fn) const { fn(_server); }
+ };
+
+ Registry > _deps;
+
+ void cut_dependencies()
+ {
+ _deps.for_each([&] (Dependency &dep) {
+ destroy(_alloc, &dep); });
+ }
+
+ /*
+ * Helper variable for calculating the layout of dependent (child) nodes
+ *
+ * This variable tracks the offset of the last visited child node.
+ * During the layouting procedure, it gets successively increased.
+ */
+ unsigned layout_breadth_child_offset = 0;
+
+ /*
+ * Breadth position relative to the node's primary dependency node.
+ */
+ unsigned layout_breadth_offset = 0;
+
+ Node(Allocator &alloc, Widget &widget) : _alloc(alloc), _widget(widget) { }
+
+ template
+ void for_each_dependent_node(FN const &fn)
+ {
+ _server_anchors.for_each([&] (Anchor &anchor) { fn(anchor._remote); });
+ }
+
+ virtual ~Node() { cut_dependencies(); }
+
+ bool has_deps() const
+ {
+ bool result = false;
+ _deps.for_each([&] (Dependency const &) { result = true; });
+ return result;
+ }
+
+ bool belongs_to(Widget const &w) { return &_widget == &w; }
+
+ bool has_name(Name const &name) const { return _widget.has_name(name); }
+
+ unsigned depth_size(Depth_direction dir) const
+ {
+ return dir.horizontal() ? _widget.min_size().w() : _widget.min_size().h();
+ }
+
+ /**
+ * Accumulate breadth of all clients of the node
+ */
+ unsigned _breadth_clients_size(Depth_direction dir) const
+ {
+ unsigned sum_clients_size = 0;
+
+ _server_anchors.for_each([&] (Anchor const &anchor) {
+ if (anchor.primary())
+ sum_clients_size += anchor._remote.breadth_size(dir); });
+
+ return sum_clients_size;
+ }
+
+ /**
+ * Return breadth of the node, including the widget and all children
+ */
+ unsigned breadth_size(Depth_direction dir) const
+ {
+ unsigned const widget_size =
+ dir.horizontal() ? _widget.min_size().h() : _widget.min_size().w();
+
+ unsigned const breadth_padding = 10;
+
+ return max(widget_size + breadth_padding, _breadth_clients_size(dir));
+ }
+
+ /**
+ * Return 'depth' position of node within the dependency tree
+ */
+ unsigned depth_pos(Depth_direction dir) const
+ {
+ /* maximum depth position of all nodes we depend on */
+ unsigned max_deps_depth = 0;
+ _deps.for_each([&] (Dependency const &dep) {
+ max_deps_depth = max(max_deps_depth, dep.server_depth_pos(dir)); });
+
+ unsigned depth_padding = 10;
+ return max_deps_depth + depth_padding;
+ }
+
+ /**
+ * Return breadth position of our primary dependency (parent) node
+ * within the dependency tree
+ */
+ unsigned _primary_dep_breadth_pos(Depth_direction dir) const
+ {
+ unsigned result = 0;
+ _deps.for_each([&] (Registered const &dep) {
+ if (dep.primary()) {
+ result = dep.server_breadth_pos(dir)
+ + dep.server_breadth_alignment(dir); } });
+ return result;
+ }
+
+ /**
+ * Return absolute 'breadth' position of node within the dependency tree
+ *
+ * This method relies on the prior computed 'layout_breadth_offset'
+ * values (of this and its chain of primary dependency nodes).
+ */
+ unsigned breadth_pos(Depth_direction dir) const
+ {
+ return _primary_dep_breadth_pos(dir) + layout_breadth_offset;
+ }
+
+ void mark_deps_as_out_of_date()
+ {
+ _deps.for_each([&] (Dependency &dep) { dep.up_to_date = false; });
+ }
+
+ void depends_on(Node &node, Anchor::Type type)
+ {
+ bool dependency_exists = false;
+ _deps.for_each([&] (Dependency &dep) {
+ if (dep.depends_on(node)) {
+ dep.up_to_date = true; /* skip in 'destroy_stale_deps' */
+ dependency_exists = true;
+ }
+ });
+
+ if (!dependency_exists)
+ new (_alloc) Registered(_deps, *this, node, type);
+ }
+
+ void destroy_stale_deps()
+ {
+ _deps.for_each([&] (Registered &dep) {
+ if (!dep.up_to_date)
+ destroy(_alloc, &dep); });
+ }
+
+ template
+ bool apply_to_primary_dependency(FN &fn)
+ {
+ bool result = false;
+ _deps.for_each([&] (Registered &dep) {
+ if (dep.primary()) {
+ dep.apply_to_server(fn);
+ result = true;
+ }
+ });
+ return result;
+ }
+
+ int centered_breadth_pos(Depth_direction dir) const
+ {
+ return dir.horizontal() ? (_widget.geometry.y1() + _widget.geometry.y2()) / 2
+ : (_widget.geometry.x1() + _widget.geometry.x2()) / 2;
+ }
+
+ unsigned _edge_size(Depth_direction dir) const
+ {
+ if (dir.horizontal())
+ return max(0, (int)_widget.geometry.h() - (int)_widget.margin.top
+ - (int)_widget.margin.bottom);
+ else
+ return max(0, (int)_widget.geometry.w() - (int)_widget.margin.left
+ - (int)_widget.margin.right);
+ }
+
+ /**
+ * Return position of connection point at node edge
+ */
+ unsigned _edge_pos(Registry > const &anchors,
+ Node const &client, Depth_direction dir) const
+ {
+ int const client_pos = client.centered_breadth_pos(dir);
+
+ /*
+ * Count number of anchors lower than the client-node position and
+ * the total number of clients. The anchor points are positioned
+ * along the widget edge in the order of the client positions to
+ * avoid intersecting dependency lines.
+ */
+ int lower_cnt = 0, total_cnt = 0;
+ anchors.for_each([&] (Anchor const &anchor) {
+ total_cnt++;
+ if (anchor.remote_centered_breadth_pos(dir) < client_pos)
+ lower_cnt++;
+ });
+
+ return ((lower_cnt + 1)*_edge_size(dir)) / (total_cnt + 1);
+ }
+
+ Point server_anchor_point(Node const &client, Depth_direction dir) const
+ {
+ int const pos = _edge_pos(_server_anchors, client, dir);
+ Rect const edges = _widget.edges();
+
+ switch (dir.value) {
+ case Depth_direction::EAST: return Point(edges.x2(), edges.y1() + pos);
+ case Depth_direction::WEST: return Point(edges.x1(), edges.y1() + pos);
+ case Depth_direction::NORTH: return Point(edges.x1() + pos, edges.y1());
+ case Depth_direction::SOUTH: return Point(edges.x1() + pos, edges.y2());
+ }
+ return Point(0, 0);
+ }
+
+ Point client_anchor_point(Node const &client, Depth_direction dir) const
+ {
+ int const pos = _edge_pos(_client_anchors, client, dir);
+ Rect const edges = _widget.edges();
+
+ switch (dir.value) {
+ case Depth_direction::EAST: return Point(edges.x1(), edges.y1() + pos);
+ case Depth_direction::WEST: return Point(edges.x2(), edges.y1() + pos);
+ case Depth_direction::NORTH: return Point(edges.x1() + pos, edges.y2());
+ case Depth_direction::SOUTH: return Point(edges.x1() + pos, edges.y1());
+ }
+ return Point(0, 0);
+ }
+ };
+
+ typedef Registered Registered_node;
+ typedef Registry Node_registry;
+
+ Node_registry _nodes;
+
+ Registered_node _root_node { _nodes, _factory.alloc, *this };
+
+ template
+ void apply_to_primary_dependency(Node &node, FN const &fn)
+ {
+ if (node.apply_to_primary_dependency(fn))
+ return;
+
+ /* node has no primary dependency defined, use root node */
+ fn(_root_node);
+ }
+
+ /**
+ * Customized model-update policy that augments the list of child widgets
+ * with their graph-node topology
+ */
+ struct Model_update_policy : List_model_update_policy
+ {
+ Widget::Model_update_policy &_generic_model_update_policy;
+ Allocator &_alloc;
+ Node_registry &_nodes;
+
+ Model_update_policy(Widget::Model_update_policy &policy,
+ Allocator &alloc, Node_registry &nodes)
+ :
+ _generic_model_update_policy(policy), _alloc(alloc), _nodes(nodes)
+ { }
+
+ void _destroy_node(Registered_node &node)
+ {
+ /*
+ * If a server node vanishes, disconnect all client nodes. The
+ * nodes will be reconnected - if possible - after the model
+ * update.
+ */
+ node.for_each_dependent_node([&] (Node &dependent) {
+ dependent.cut_dependencies(); });
+
+ Widget &w = node._widget;
+ destroy(_alloc, &node);
+ _generic_model_update_policy.destroy_element(w);
+ }
+
+ void destroy_element(Widget &w)
+ {
+ _nodes.for_each([&] (Registered_node &node) {
+ if (node.belongs_to(w))
+ _destroy_node(node); });
+ }
+
+ /* do not import nodes as widgets */
+ bool node_is_element(Xml_node node) { return !node.has_type("dep"); }
+
+ Widget &create_element(Xml_node elem_node)
+ {
+ Widget &w = _generic_model_update_policy.create_element(elem_node);
+ new (_alloc) Registered_node(_nodes, _alloc, w);
+ return w;
+ }
+
+ void update_element(Widget &w, Xml_node elem_node)
+ {
+ _generic_model_update_policy.update_element(w, elem_node);
+ }
+
+ static bool element_matches_xml_node(Widget const &w, Xml_node node)
+ {
+ return Widget::Model_update_policy::element_matches_xml_node(w, node);
+ }
+
+ } _model_update_policy { Widget::_model_update_policy, _factory.alloc, _nodes };
+
+ Depgraph_widget(Widget_factory &factory, Xml_node node, Unique_id unique_id)
+ :
+ Widget(factory, node, unique_id)
+ { }
+
+ ~Depgraph_widget()
+ {
+ while (Widget *w = _children.first()) {
+ _children.remove(w);
+ _model_update_policy.destroy_element(*w);
+ }
+ }
+
+ void update(Xml_node node) override
+ {
+ /* update depth direction */
+ {
+ typedef String<10> Dir_name;
+ Dir_name dir_name = node.attribute_value("direction", Dir_name());
+ _depth_direction = { Depth_direction::EAST };
+ if (dir_name == "north") _depth_direction = { Depth_direction::NORTH };
+ if (dir_name == "south") _depth_direction = { Depth_direction::SOUTH };
+ if (dir_name == "east") _depth_direction = { Depth_direction::EAST };
+ if (dir_name == "west") _depth_direction = { Depth_direction::WEST };
+ }
+
+ update_list_model_from_xml(_model_update_policy, _children, node);
+
+ /*
+ * Import dependencies
+ */
+ _nodes.for_each([&] (Node &node) {
+ node.mark_deps_as_out_of_date(); });
+
+ node.for_each_sub_node([&] (Xml_node node) {
+
+ bool const primary = !node.has_type("dep");
+
+ typedef String<64> Node_name;
+ Node_name client_name, server_name;
+ if (primary) {
+ client_name = node.attribute_value("name", Node_name());
+ server_name = node.attribute_value("dep", Node_name());
+ } else {
+ client_name = node.attribute_value("node", Node_name());
+ server_name = node.attribute_value("on", Node_name());
+ }
+
+ if (!server_name.valid())
+ return;
+
+ Node *client = nullptr, *server = nullptr;
+ _nodes.for_each([&] (Node &node) {
+ if (node.has_name(client_name)) client = &node;
+ if (node.has_name(server_name)) server = &node;
+ });
+
+ if (client && server && client != server)
+ client->depends_on(*server, primary ? Node::Anchor::PRIMARY
+ : Node::Anchor::SECONDARY);
+ if (client && !server) {
+ warning("node '", client_name, "' depends on "
+ "non-existing node '", server_name, "'");
+ client->_widget.geometry = Rect(Point(0, 0), Area(0, 0));
+ }
+ });
+
+ _nodes.for_each([&] (Node &node) {
+ node.destroy_stale_deps(); });
+
+ _nodes.for_each([&] (Node &node) {
+ node.layout_breadth_child_offset = 0; });
+
+ /*
+ * Compute 'layout_breadth_offset' values of all nodes
+ *
+ * The computation depends on the order of '_children'.
+ */
+ for (Widget *w = _children.first(); w; w = w->next()) {
+ _nodes.for_each([&] (Registered_node &node) {
+ if (!node.belongs_to(*w))
+ return;
+
+ apply_to_primary_dependency(node, [&] (Node &parent) {
+
+ node.layout_breadth_offset = parent.layout_breadth_child_offset;
+
+ /* advance breadth offset at parent by size of current node */
+ parent.layout_breadth_child_offset +=
+ node.breadth_size(_depth_direction);
+ });
+ });
+ }
+
+ /*
+ * Apply layout to the children, determine _min_size
+ */
+ Rect bounding_box(Point(0, 0), Area(0, 0));
+ for (Widget *w = _children.first(); w; w = w->next()) {
+
+ _nodes.for_each([&] (Registered_node &node) {
+ if (!node.belongs_to(*w))
+ return;
+
+ int const depth_pos = node.depth_pos(_depth_direction),
+ breadth_pos = node.breadth_pos(_depth_direction),
+ depth_size = node.depth_size(_depth_direction),
+ breadth_size = node.breadth_size(_depth_direction);
+
+ Rect const node_rect = _depth_direction.horizontal()
+ ? Rect(Point(depth_pos, breadth_pos),
+ Area(depth_size, breadth_size))
+ : Rect(Point(breadth_pos, depth_pos),
+ Area(breadth_size, depth_size));
+
+ w->geometry = Rect(node_rect.center(w->min_size()), w->min_size());
+
+ bounding_box = Rect::compound(bounding_box, w->geometry);
+ });
+ }
+
+ /*
+ * Mirror coordinates if graph grows towards north or west
+ */
+ if (_depth_direction.value == Depth_direction::NORTH
+ || _depth_direction.value == Depth_direction::WEST) {
+
+ for (Widget *w = _children.first(); w; w = w->next()) {
+
+ int x = w->geometry.x1(), y = w->geometry.y1();
+
+ if (_depth_direction.value == Depth_direction::NORTH)
+ y = (int)bounding_box.h() - y - w->geometry.h();
+
+ if (_depth_direction.value == Depth_direction::WEST)
+ x = (int)bounding_box.w() - x - w->geometry.w();
+
+ w->geometry = Rect(Point(x, y), w->geometry.area());
+ }
+ }
+ _min_size = bounding_box.area();
+ }
+
+ Area min_size() const override { return _min_size; }
+
+ void _draw_connect(Surface &pixel_surface,
+ Surface &alpha_surface,
+ Point p1, Point p2, Color color, bool horizontal) const
+ {
+ Line_painter line_painter;
+
+ auto draw_segment = [&] (long x1, long y1, long x2, long y2)
+ {
+ auto const fx1 = Line_painter::Fixpoint::from_raw(x1),
+ fy1 = Line_painter::Fixpoint::from_raw(y1),
+ fx2 = Line_painter::Fixpoint::from_raw(x2),
+ fy2 = Line_painter::Fixpoint::from_raw(y2);
+
+ line_painter.paint(pixel_surface, fx1, fy1, fx2, fy2, color);
+ line_painter.paint(alpha_surface, fx1, fy1, fx2, fy2, color);
+ };
+
+ long const mid_x = (p1.x() + p2.x()) / 2,
+ mid_y = (p1.y() + p2.y()) / 2;
+
+ long const x1 = p1.x(),
+ y1 = p1.y(),
+ x2 = horizontal ? mid_x : p1.x(),
+ y2 = horizontal ? p1.y() : mid_y,
+ x3 = horizontal ? mid_x : p2.x(),
+ y3 = horizontal ? p2.y() : mid_y,
+ x4 = p2.x(),
+ y4 = p2.y();
+
+ /* subdivide the curve depending on the size of its bounding box */
+ unsigned const levels = max(log2(max(abs(x4 - x1), abs(y4 - y1)) >> 2), 3);
+
+ bezier(x1 << 8, y1 << 8, x2 << 8, y2 << 8,
+ x3 << 8, y3 << 8, x4 << 8, y4 << 8, draw_segment, levels);
+ }
+
+ void _draw_connections(Surface &pixel_surface,
+ Surface &alpha_surface,
+ Point at, bool shadow) const
+ {
+ _nodes.for_each([&] (Node const &client) {
+
+ client._deps.for_each([&] (Node::Dependency const &dep) {
+
+ Color color;
+
+ if (shadow) {
+ color = dep.primary() ? Color(0, 0, 0, 150)
+ : Color(0, 0, 0, 50);
+ } else {
+ color = dep.primary() ? Color(255, 255, 255, 190)
+ : Color(255, 255, 255, 120);
+ }
+
+ dep.apply_to_server([&] (Node const &server) {
+
+ Point from = server.server_anchor_point(client, _depth_direction);
+ Point to = client.client_anchor_point(server, _depth_direction);
+
+ _draw_connect(pixel_surface, alpha_surface,
+ at + from, at + to, color, _depth_direction.horizontal());
+ });
+ });
+ });
+ }
+
+ void draw(Surface &pixel_surface,
+ Surface &alpha_surface,
+ Point at) const
+ {
+ /* draw connections twice, for the shadow and actual color */
+ _draw_connections(pixel_surface, alpha_surface, at + Point(0, 1), true);
+ _draw_connections(pixel_surface, alpha_surface, at, false);
+
+ _draw_children(pixel_surface, alpha_surface, at);
+ }
+
+ void _layout() override
+ {
+ for (Widget *w = _children.first(); w; w = w->next())
+ w->size(w->geometry.area());
+ }
+};
+
+#endif /* _DEPGRAPH_WIDGET_H_ */
diff --git a/repos/gems/src/app/menu_view/main.cc b/repos/gems/src/app/menu_view/main.cc
index 6de4894656..3ffc4e7dc6 100644
--- a/repos/gems/src/app/menu_view/main.cc
+++ b/repos/gems/src/app/menu_view/main.cc
@@ -19,6 +19,7 @@
#include "root_widget.h"
#include "float_widget.h"
#include "frame_widget.h"
+#include "depgraph_widget.h"
/* Genode includes */
#include
@@ -302,12 +303,13 @@ Menu_view::Widget_factory::create(Xml_node node)
Widget::Unique_id const unique_id(++_unique_id_cnt);
- if (node.has_type("label")) w = new (alloc) Label_widget (*this, node, unique_id);
- if (node.has_type("button")) w = new (alloc) Button_widget (*this, node, unique_id);
- if (node.has_type("vbox")) w = new (alloc) Box_layout_widget (*this, node, unique_id);
- if (node.has_type("hbox")) w = new (alloc) Box_layout_widget (*this, node, unique_id);
- if (node.has_type("frame")) w = new (alloc) Frame_widget (*this, node, unique_id);
- if (node.has_type("float")) w = new (alloc) Float_widget (*this, node, unique_id);
+ if (node.has_type("label")) w = new (alloc) Label_widget (*this, node, unique_id);
+ if (node.has_type("button")) w = new (alloc) Button_widget (*this, node, unique_id);
+ if (node.has_type("vbox")) w = new (alloc) Box_layout_widget (*this, node, unique_id);
+ if (node.has_type("hbox")) w = new (alloc) Box_layout_widget (*this, node, unique_id);
+ if (node.has_type("frame")) w = new (alloc) Frame_widget (*this, node, unique_id);
+ if (node.has_type("float")) w = new (alloc) Float_widget (*this, node, unique_id);
+ if (node.has_type("depgraph")) w = new (alloc) Depgraph_widget (*this, node, unique_id);
if (!w) {
Genode::error("unknown widget type '", node.type(), "'");
diff --git a/repos/gems/src/app/menu_view/widget.h b/repos/gems/src/app/menu_view/widget.h
index 2c199fbe98..38c7c6b91b 100644
--- a/repos/gems/src/app/menu_view/widget.h
+++ b/repos/gems/src/app/menu_view/widget.h
@@ -158,6 +158,15 @@ class Menu_view::Widget : public List::Element
*/
Rect geometry;
+ /*
+ * Return x/y positions of the edges of the widget with the margin
+ * applied
+ */
+ Rect edges() const { return Rect(Point(geometry.x1() + margin.left,
+ geometry.y1() + margin.top),
+ Point(geometry.x2() - margin.right,
+ geometry.y2() - margin.bottom)); }
+
Widget(Widget_factory &factory, Xml_node node, Unique_id unique_id)
:
_type_name(node_type_name(node)),