genode/repos/gems/src/app/menu_view/depgraph_widget.h

662 lines
19 KiB
C++

/*
* \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 <base/registry.h>
/* gems includes */
#include <polygon_gfx/line_painter.h>
/* 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<Registered<Anchor> > _server_anchors { };
Registry<Registered<Anchor> > _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> _anchor_at_server;
Registered<Anchor> _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 <typename FN>
void apply_to_server(FN const &fn) { fn(_server); }
template <typename FN>
void apply_to_server(FN const &fn) const { fn(_server); }
};
Registry<Registered<Dependency> > _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 <typename FN>
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<Dependency> 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<Dependency>(_deps, *this, node, type);
}
void destroy_stale_deps()
{
_deps.for_each([&] (Registered<Dependency> &dep) {
if (!dep.up_to_date)
destroy(_alloc, &dep); });
}
template <typename FN>
bool apply_to_primary_dependency(FN &fn)
{
bool result = false;
_deps.for_each([&] (Registered<Dependency> &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<Registered<Anchor> > 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<Node> Registered_node;
typedef Registry<Registered_node> Node_registry;
Node_registry _nodes { };
Registered_node _root_node { _nodes, _factory.alloc, *this };
template <typename FN>
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<Widget>::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 <dep> 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()
{
_children.destroy_all_elements(_model_update_policy);
}
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 };
}
_children.update_from_xml(_model_update_policy, 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'.
*/
_children.for_each([&] (Widget &w) {
_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));
_children.for_each([&] (Widget &w) {
_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) {
_children.for_each([&] (Widget &w) {
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_rgb888> &pixel_surface,
Surface<Pixel_alpha8> &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_rgb888> &pixel_surface,
Surface<Pixel_alpha8> &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_rgb888> &pixel_surface,
Surface<Pixel_alpha8> &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
{
_children.for_each([&] (Widget &w) {
w.size(w.geometry().area()); });
}
};
#endif /* _DEPGRAPH_WIDGET_H_ */