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)),