├── LICENSE
├── README.md
├── data
├── css
│ └── flow.css
├── libflow.gresource.xml
├── meson.build
└── ui
│ ├── node-view.blp
│ └── node.blp
├── example
├── main.vala
└── meson.build
├── libflow.doap
├── meson.build
├── result.png
├── src
├── css-loader.vala
├── deps.in
├── meson.build
├── minimap.vala
├── node-view.vala
├── node.vala
├── renderers
│ ├── connection-renderer.vala
│ └── socket-renderer.vala
├── rubberband.vala
├── sink.vala
├── socket.vala
├── source.vala
└── title-style.vala
└── tests
├── meson.build
└── node-view-test.vala
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 PaladinDev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # libflow
2 | [](https://t.me/vala_lang)
3 | Flow Graph view for Gtk4
4 |
5 | 
6 |
7 | ## Installation
8 |
9 | ### ArchLinux (AUR)
10 |
11 | $ yay -S libflow-git
12 |
13 | ### Build manualy
14 |
15 | $ meson setup --prefix=/usr --buildtype=release build
16 | $ ninja -C build install
17 |
--------------------------------------------------------------------------------
/data/css/flow.css:
--------------------------------------------------------------------------------
1 | node {
2 | background-color: @card_bg_color;
3 | color: @card_fg_color;
4 | border-radius: 12px;
5 | box-shadow:
6 | 0 0 0 1px rgba(0, 0, 0, 0.03),
7 | 0 1px 3px 1px rgba(0, 0, 0, 0.07),
8 | 0 2px 6px 2px rgba(0, 0, 0, 0.03);
9 | }
10 |
11 | node:selected {
12 | background-color: alpha(@accent_color, 0.2);
13 | }
14 |
15 | node #title-container {
16 | border-radius: 12px 12px 0px 0px;
17 | padding: 10px;
18 | }
19 |
20 | node #title-container.shadow {
21 | box-shadow: 0 1px rgba(0,0,0,0.18), 0 2px 4px rgba(0,0,0,0.18);
22 | background-color: @card_bg_color;
23 | }
24 |
25 | node #title-container.separator {
26 | border-bottom: 1px solid alpha(currentColor,0.15);
27 | }
28 |
29 | node #socket-container {
30 | padding: 10px;
31 | padding-top: 5px;
32 | padding-bottom: 5px;
33 | border-spacing: 5px;
34 | }
35 |
36 | node #sink-container, #source-container {
37 | border-spacing: 5px;
38 | }
39 |
40 | node #content {
41 | padding: 10px;
42 | padding-top: 0px;
43 | }
44 |
45 | rubberband {
46 | border-radius: 8px;
47 | }
48 |
49 |
--------------------------------------------------------------------------------
/data/libflow.gresource.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | css/flow.css
5 | ui/node.ui
6 | ui/node-view.ui
7 |
8 |
9 |
--------------------------------------------------------------------------------
/data/meson.build:
--------------------------------------------------------------------------------
1 | blueprints = files(
2 | 'ui/node.blp',
3 | 'ui/node-view.blp',
4 | )
5 |
6 | blp_target = custom_target(
7 | 'blueprints',
8 |
9 | input: blueprints,
10 | output: '.',
11 | command: [ find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@' ],
12 | )
13 |
14 | lib_resources = gnome.compile_resources(
15 | 'libflow-resources',
16 | 'libflow.gresource.xml',
17 | dependencies: blp_target
18 | )
19 |
20 | if get_option('buildtype') == 'debug'
21 | # Don't do it in debug mode to make VLS works
22 | compiler_dependency = []
23 | else
24 | # Creates empty vala file to fix build order issue with blueprints
25 | compiler_dependency = custom_target(
26 | 'compiler_dependency',
27 |
28 | depends: lib_resources,
29 | output: 'dependency.vala',
30 | command: [ find_program('touch'), '@OUTPUT@' ]
31 | )
32 | endif
--------------------------------------------------------------------------------
/data/ui/node-view.blp:
--------------------------------------------------------------------------------
1 | using Gtk 4.0;
2 |
3 | template $FlowNodeView : Widget {
4 |
5 | GestureDrag drag {
6 | drag-begin => $start_drag();
7 | drag-update => $process_drag();
8 | drag-end => $stop_drag();
9 | }
10 |
11 | GestureClick secondary_click {
12 | button: 3;
13 | pressed => $open_menu();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/data/ui/node.blp:
--------------------------------------------------------------------------------
1 | using Gtk 4.0;
2 |
3 | template $FlowNode : $FlowNodeRenderer {
4 |
5 | Box main_box {
6 | name: "node-container";
7 | orientation: vertical;
8 |
9 | Box title_box {
10 | name: "title-container";
11 | orientation: vertical;
12 | }
13 |
14 | Box socket_box {
15 | name: "socket-container";
16 | orientation: horizontal;
17 |
18 | Box sink_box {
19 | name: "sink-container";
20 | orientation: vertical;
21 | hexpand: true;
22 | }
23 |
24 | Box source_box {
25 | name: "source-container";
26 | orientation: vertical;
27 | }
28 | }
29 |
30 | Box content_box {
31 | name: "content";
32 | orientation: vertical;
33 | visible: false;
34 | }
35 | }
36 |
37 | PopoverMenu menu {
38 | has-arrow: false;
39 | menu-model: delete_menu;
40 | }
41 |
42 | GestureDrag drag {
43 | drag-begin => $begin_drag();
44 | }
45 |
46 | GestureClick secondary_click {
47 | button: 3;
48 | pressed => $open_menu();
49 | }
50 | }
51 |
52 | menu delete_menu {
53 | item {
54 | label: _("Delete");
55 | action: "node.delete";
56 | }
57 |
58 | item {
59 | label: _("Unlink all");
60 | action: "node.unlink-all";
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/example/main.vala:
--------------------------------------------------------------------------------
1 | public class NumberGeneratorNode : Flow.Node {
2 |
3 | public NumberGeneratorNode() {
4 | add_css_class("green");
5 | title_style = Flow.TitleStyle.SEPARATOR;
6 | set_label_name("NumberGenerator", Gtk.Align.START);
7 | highlight_color = { 0.20F, 0.82F, 0.48F, 0.3F };
8 |
9 | var number_source = new Flow.Source.with_type(Type.DOUBLE) {
10 | color = { 1, 1, 0, 1 },
11 | name = "output"
12 | };
13 | number_source.set_value(0d);
14 | add_source(number_source);
15 |
16 | var spin_button = new Gtk.SpinButton(new Gtk.Adjustment(0, 0, 100, 1, 10, 0), 0, 0);
17 | spin_button.set_size_request(50,20);
18 | spin_button.value_changed.connect(() => {
19 | number_source.set_value(spin_button.get_value());
20 | });
21 |
22 | content = spin_button;
23 | }
24 | }
25 |
26 | public class OperationNode : Flow.Node {
27 | private double operand_a_value = 0;
28 | private double operand_b_value = 0;
29 | private string operation = "+";
30 |
31 | private Flow.Source result;
32 |
33 | public OperationNode() {
34 | add_css_class("yellow");
35 | title_style = Flow.TitleStyle.SEPARATOR;
36 | set_label_name("Operation", Gtk.Align.START);
37 | highlight_color = { 0.96F, 0.83F, 0.18F, 0.3F };
38 |
39 | result = new Flow.Source.with_type(Type.DOUBLE) {
40 | color = { 1, 0, 1, 1 },
41 | name = "result"
42 | };
43 |
44 | var summand_a = new Flow.Sink.with_type(Type.DOUBLE) {
45 | color = { 0, 1, 1, 1 },
46 | name = "operand A"
47 | };
48 | summand_a.changed.connect(@value => {
49 | if (@value == null) {
50 | return;
51 | }
52 | operand_a_value = @value.get_double();
53 | publish_result();
54 | });
55 |
56 | var summand_b = new Flow.Sink.with_type(Type.DOUBLE) {
57 | color = { 0, 1, 0, 1 },
58 | name = "operand B"
59 | };
60 | summand_b.changed.connect(@value => {
61 | if (@value == null)
62 | return;
63 |
64 | operand_b_value = @value.get_double();
65 | publish_result();
66 | });
67 |
68 | add_source(result);
69 | add_sink(summand_a);
70 | add_sink(summand_b);
71 |
72 | string[] operations = { "+", "-", "*", "/" };
73 | var drop_down = new Gtk.DropDown.from_strings(operations);
74 | drop_down.notify["selected-item"].connect(() => {
75 | operation = operations[drop_down.get_selected()];
76 | publish_result();
77 | });
78 |
79 | content = drop_down;
80 | }
81 |
82 | private void publish_result() {
83 | if (operation == "+") {
84 | set_result(operand_a_value + operand_b_value);
85 | } else if (operation == "-") {
86 | set_result(operand_a_value - operand_b_value);
87 | } else if (operation == "*") {
88 | set_result(operand_a_value * operand_b_value);
89 | } else if (operation == "/") {
90 | set_result(operand_a_value / operand_b_value);
91 | }
92 | }
93 |
94 | private void set_result(double operation_result) {
95 | result.set_value(operation_result);
96 | }
97 | }
98 |
99 | public class PrintNode : Flow.Node {
100 | public Gtk.Label label;
101 |
102 | public PrintNode() {
103 | add_css_class("blue");
104 | title_style = Flow.TitleStyle.SEPARATOR;
105 | set_label_name("Output", Gtk.Align.START);
106 | highlight_color = { 0.21F, 0.52F, 0.89F, 0.3f };
107 |
108 | var number = new Flow.Sink.with_type(Type.DOUBLE) {
109 | color = { 0, 0, 1, 1 },
110 | name = "input"
111 | };
112 | number.changed.connect(display_value);
113 |
114 | add_sink(number);
115 |
116 | content = label = new Gtk.Label("");
117 | }
118 |
119 | private void display_value(Value? @value) {
120 | if (@value == null) {
121 | return;
122 | }
123 |
124 | var text = @value.strdup_contents();
125 | var dot_index = text.index_of_char('.', 0);
126 |
127 | if (text.get_char(dot_index + 1) == '0')
128 | text = text.substring(0, dot_index);
129 | else
130 | text = text.substring(0, dot_index + 2);
131 |
132 | label.set_text(text);
133 | }
134 | }
135 |
136 | public class AdvancedCalculatorWindow : Gtk.ApplicationWindow {
137 | private Flow.NodeView node_view;
138 | private Gtk.Box menu_content;
139 |
140 | public AdvancedCalculatorWindow(Gtk.Application app) {
141 | application = app;
142 |
143 | set_default_size(600, 550);
144 | init_header_bar();
145 | init_window_layout();
146 | init_actions();
147 | init_stylesheet();
148 | }
149 |
150 | private void init_header_bar() {
151 | set_titlebar(new Gtk.HeaderBar() {
152 | title_widget = new Gtk.Label("libflow Example") {
153 | use_markup = true
154 | }
155 | });
156 | }
157 |
158 | private void init_window_layout() {
159 | Gtk.Overlay overlay;
160 |
161 | set_child(overlay = new Gtk.Overlay() {
162 | child = new Gtk.ScrolledWindow() {
163 | child = this.node_view = new Flow.NodeView()
164 | }
165 | });
166 |
167 | overlay.add_overlay(new Flow.Minimap() {
168 | halign = Gtk.Align.END,
169 | valign = Gtk.Align.END,
170 | nodeview = node_view,
171 | can_target = false
172 | });
173 | }
174 |
175 | private void init_actions() {
176 | menu_content = new Gtk.Box(Gtk.Orientation.VERTICAL, 10);
177 | node_view.menu_content = menu_content;
178 |
179 | add_number_node_action();
180 | add_operation_node_action();
181 | add_print_node_action();
182 | }
183 |
184 | private void init_stylesheet() {
185 | var provider = new Gtk.CssProvider();
186 | provider.load_from_string(
187 | """node.green #title-container {
188 | background-color: alpha(@green_3, 0.2);
189 | color: @green_3;
190 | }
191 | node.yellow #title-container {
192 | background-color: alpha(@yellow_3, 0.2);
193 | color: @yellow_3;
194 | }
195 | node.blue #title-container {
196 | background-color: alpha(@blue_3, 0.2);
197 | color: @blue_3;
198 | }
199 | """
200 | );
201 |
202 | Gtk.StyleContext.add_provider_for_display(
203 | Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
204 | );
205 | }
206 |
207 | private void add_number_node_action() {
208 | var button = new Gtk.Button.with_label("NumberGenerator");
209 | button.clicked.connect(() => {
210 | node_view.add(new NumberGeneratorNode() { x = 20, y = 20 });
211 | });
212 | button.set_has_frame(false);
213 | menu_content.append(button);
214 | }
215 |
216 | private void add_operation_node_action() {
217 | var button = new Gtk.Button.with_label("Operation");
218 | button.clicked.connect(() => {
219 | node_view.add(new OperationNode() { x = 200, y = 20 });
220 | });
221 | button.set_has_frame(false);
222 | menu_content.append(button);
223 | }
224 |
225 | private void add_print_node_action() {
226 | var button = new Gtk.Button.with_label("Print");
227 | button.clicked.connect(() => {
228 | node_view.add(new PrintNode() { x = 400, y = 20 });
229 | });
230 | button.set_has_frame(false);
231 | menu_content.append(button);
232 | }
233 | }
234 |
235 | int main(string[] argv) {
236 | var app = new Gtk.Application(
237 | "com.example.GtkApplication",
238 | ApplicationFlags.FLAGS_NONE
239 | );
240 |
241 | app.activate.connect(() => new AdvancedCalculatorWindow(app).present());
242 |
243 | return app.run(argv);
244 | }
245 |
--------------------------------------------------------------------------------
/example/meson.build:
--------------------------------------------------------------------------------
1 | executable(
2 | 'libflow-sample',
3 |
4 | files('main.vala'),
5 | vala_args: '--target-glib=2.58',
6 | dependencies: [
7 | dependency('gtk4'),
8 | lib_dependency
9 | ]
10 | )
11 |
--------------------------------------------------------------------------------
/libflow.doap:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 | libflow
10 | FlowGraph view for Gtk4
11 | Library to create Flow Graphs for Gtk4
12 |
13 |
14 |
15 | Vala
16 |
17 |
18 |
19 | PaladinDev
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/meson.build:
--------------------------------------------------------------------------------
1 | project(
2 | 'libflow',
3 |
4 | [ 'c', 'vala' ],
5 | version: '1.0.0',
6 | meson_version: '>= 0.50.0',
7 | )
8 |
9 | pkg = import('pkgconfig')
10 | gnome = import('gnome')
11 |
12 | api_version = '1.0'
13 | lib_name = meson.project_name() + '-' + api_version
14 | gir_name = 'Flow-' + api_version
15 |
16 | subdir('data')
17 | subdir('src')
18 |
19 | subdir('tests')
20 |
21 | subdir('example')
22 |
--------------------------------------------------------------------------------
/result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SpikedPaladin/libflow/3abd447a0d2885663b5c307ae366c7e696f5eb82/result.png
--------------------------------------------------------------------------------
/src/css-loader.vala:
--------------------------------------------------------------------------------
1 | namespace Flow {
2 |
3 | [SingleInstance]
4 | protected class CssLoader : Object {
5 | public Gtk.CssProvider provider;
6 |
7 | public void ensure() {
8 | if (provider != null) return;
9 |
10 | provider = new Gtk.CssProvider();
11 | provider.load_from_resource("/me/paladin/libflow/css/flow.css");
12 | Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/deps.in:
--------------------------------------------------------------------------------
1 | gtk4
--------------------------------------------------------------------------------
/src/meson.build:
--------------------------------------------------------------------------------
1 | lib_deps = [
2 | dependency('gtk4')
3 | ]
4 |
5 | lib_sources = files(
6 | 'css-loader.vala',
7 | 'minimap.vala',
8 | 'node.vala',
9 | 'node-view.vala',
10 | 'rubberband.vala',
11 | 'sink.vala',
12 | 'socket.vala',
13 | 'source.vala',
14 | 'title-style.vala',
15 |
16 | 'renderers/connection-renderer.vala',
17 | 'renderers/socket-renderer.vala',
18 | )
19 |
20 | install_data(
21 | 'deps.in',
22 |
23 | rename: lib_name + '.deps',
24 | install_dir: get_option('datadir') / 'vala' / 'vapi'
25 | )
26 |
27 | lib = library(
28 | lib_name,
29 |
30 | compiler_dependency, # Required to build blueprints first
31 | lib_resources,
32 | lib_sources,
33 | dependencies: lib_deps,
34 | vala_gir: gir_name + '.gir',
35 | vala_args: [ '--gresourcesdir=data/' ],
36 | install: true,
37 | install_dir: [true, true, true, true]
38 | )
39 |
40 | pkg.generate(
41 | name: meson.project_name(),
42 | description: 'Flow Graph library for Gtk4',
43 |
44 | libraries: lib,
45 | version: meson.project_version(),
46 | subdirs: lib_name,
47 | filebase: lib_name,
48 | requires: ['gtk4']
49 | )
50 |
51 | custom_target(
52 | 'typelib',
53 |
54 | command: [
55 | find_program('g-ir-compiler'),
56 |
57 | '--shared-library=' + lib.full_path().split('/')[-1],
58 | '--output=@OUTPUT@',
59 | meson.current_build_dir() / (gir_name + '.gir')
60 | ],
61 | output: gir_name + '.typelib',
62 | depends: lib,
63 | install: true,
64 | install_dir: get_option('libdir') / 'girepository-1.0'
65 | )
66 |
67 | lib_dependency = declare_dependency(
68 | link_with: lib,
69 | include_directories: include_directories('.')
70 | )
71 |
--------------------------------------------------------------------------------
/src/minimap.vala:
--------------------------------------------------------------------------------
1 | namespace Flow {
2 |
3 | /**
4 | * A Widget that draws a minmap of a {@link Flow.NodeView}
5 | *
6 | * Please set the nodeview property after integrating the referenced
7 | * {@link Flow.NodeView} into its respective container
8 | */
9 | public class Minimap : Gtk.DrawingArea {
10 | private NodeView? _nodeview = null;
11 | private Gtk.ScrolledWindow? _scrolledwindow = null;
12 | private Gtk.EventControllerMotion ctr_motion;
13 | private Gtk.GestureClick ctr_click;
14 |
15 | private ulong draw_signal = 0;
16 | private ulong hadjustment_signal = 0;
17 | private ulong vadjustment_signal = 0;
18 |
19 | private int offset_x = 0;
20 | private int offset_y = 0;
21 | private double ratio = 0.0;
22 | private int rubber_width = 0;
23 | private int rubber_height = 0;
24 | private bool move_rubber = false;
25 | /**
26 | * The nodeview that this Minimap should depict
27 | *
28 | * You may either add a {@link Flow.NodeView} directly or a
29 | * {@link Gtk.ScrolledWindow} that contains a {@link Flow.NodeView}
30 | * as its child.
31 | */
32 | public NodeView nodeview {
33 | get {
34 | return _nodeview;
35 | }
36 | set {
37 | if (_nodeview != null)
38 | SignalHandler.disconnect(_nodeview, draw_signal);
39 |
40 | if (_scrolledwindow != null) {
41 | SignalHandler.disconnect(_nodeview, hadjustment_signal);
42 | SignalHandler.disconnect(_nodeview, vadjustment_signal);
43 | }
44 | if (value == null) {
45 | _nodeview = null;
46 | _scrolledwindow = null;
47 | } else {
48 | _nodeview = value;
49 | _scrolledwindow = null;
50 | if (value.parent is Gtk.ScrolledWindow) {
51 | _scrolledwindow = value.parent as Gtk.ScrolledWindow;
52 | } else {
53 | if (value.parent is Gtk.Viewport) {
54 | if (value.parent.parent is Gtk.ScrolledWindow) {
55 | _scrolledwindow = value.parent.parent as Gtk.ScrolledWindow;
56 | hadjustment_signal = _scrolledwindow.hadjustment.notify["value"].connect(queue_draw);
57 | vadjustment_signal = _scrolledwindow.vadjustment.notify["value"].connect(queue_draw);
58 | }
59 | }
60 | }
61 | draw_signal = _nodeview.draw_minimap.connect(queue_draw);
62 | }
63 | queue_draw();
64 | }
65 | }
66 |
67 | static construct {
68 | set_css_name("minimap");
69 | }
70 |
71 | /**
72 | * Create a new Minimap
73 | */
74 | public Minimap() {
75 | set_size_request(50, 50);
76 |
77 | ctr_motion = new Gtk.EventControllerMotion();
78 | add_controller(ctr_motion);
79 | ctr_click = new Gtk.GestureClick();
80 | add_controller(ctr_click);
81 |
82 | ctr_click.pressed.connect((n, x, y) => do_button_press_event(x, y));
83 | ctr_click.end.connect(do_button_release_event);
84 | ctr_motion.motion.connect(do_motion_notify_event);
85 | }
86 |
87 | private void do_button_press_event(double x, double y) {
88 | move_rubber = true;
89 | double halloc = double.max(0, x - offset_x - rubber_width / 2) * ratio;
90 | double valloc = double.max(0, y - offset_y - rubber_height / 2) * ratio;
91 | _scrolledwindow.hadjustment.value = halloc;
92 | _scrolledwindow.vadjustment.value = valloc;
93 | }
94 |
95 | private void do_motion_notify_event(double x, double y) {
96 | if (!move_rubber || _scrolledwindow == null) {
97 | return;
98 | }
99 | double halloc = double.max(0, x - offset_x - rubber_width / 2) * ratio;
100 | double valloc = double.max(0, y - offset_y - rubber_height / 2) * ratio;
101 | _scrolledwindow.hadjustment.value = halloc;
102 | _scrolledwindow.vadjustment.value = valloc;
103 | }
104 |
105 | private void do_button_release_event() {
106 | move_rubber = false;
107 | }
108 |
109 | /**
110 | * Draws the minimap. This method is called internally
111 | */
112 | public override void snapshot(Gtk.Snapshot snapshot) {
113 | Graphene.Rect rect;
114 |
115 | if (_nodeview != null) {
116 | offset_x = 0;
117 | offset_y = 0;
118 | int height = 0;
119 | int width = 0;
120 | if (get_width() > get_height()) {
121 | width = (int) ((double) _nodeview.get_width() / _nodeview.get_height() * get_height());
122 | height = get_height();
123 | offset_x = (get_width() - width) / 2;
124 | } else {
125 | height = (int) ((double) _nodeview.get_height() / _nodeview.get_width() * get_width());
126 | width = get_width();
127 | offset_y = (get_height() - height) / 2;
128 | }
129 | ratio = (double) _nodeview.get_width() / width;
130 |
131 | for (var child = _nodeview.get_first_child(); child != null; child = child.get_next_sibling()) {
132 | if (!(child is NodeRenderer))
133 | continue;
134 |
135 | var node = (Node) child;
136 |
137 | Gdk.RGBA color;
138 | Graphene.Rect bounds;
139 | node.compute_bounds(_nodeview, out bounds);
140 |
141 | if (node.highlight_color != null) {
142 | color = node.highlight_color;
143 | } else {
144 | color = { 0.4f, 0.4f, 0.4f, 0.5f };
145 | }
146 |
147 | rect = Graphene.Rect().init(
148 | (int) (offset_x + bounds.origin.x / ratio),
149 | (int) (offset_y + bounds.origin.y / ratio),
150 | (int) (bounds.size.width / ratio),
151 | (int) (bounds.size.height / ratio)
152 | );
153 |
154 | snapshot.append_color(color, rect);
155 | }
156 | if (_scrolledwindow != null) {
157 | if (_scrolledwindow.get_width() < _nodeview.get_width() || _scrolledwindow.get_height() < _nodeview.get_height()) {
158 | rect = Graphene.Rect().init(
159 | (int) (offset_x + _scrolledwindow.hadjustment.value / ratio),
160 | (int) (offset_y + _scrolledwindow.vadjustment.value / ratio),
161 | (int) (_scrolledwindow.get_width() / ratio),
162 | (int) (_scrolledwindow.get_height() / ratio)
163 | );
164 |
165 | snapshot.append_color({ 0.0f, 0.2f, 0.6f, 0.5f }, rect);
166 | }
167 | }
168 | }
169 | }
170 | }
171 | }
172 |
173 |
--------------------------------------------------------------------------------
/src/node-view.vala:
--------------------------------------------------------------------------------
1 | namespace Flow {
2 |
3 | [GtkTemplate (ui = "/me/paladin/libflow/ui/node-view.ui")]
4 | public class NodeView : Gtk.Widget {
5 | [GtkChild]
6 | private unowned Gtk.GestureDrag drag;
7 | private Gtk.Popover menu;
8 | private Gtk.Widget _menu_content;
9 | private int _grid_size = 1;
10 | /**
11 | * If this property is set to true, the nodeview will not perform
12 | * any check wheter newly created connections will result in cycles
13 | * in the graph. It's completely up to the application programmer
14 | * to make sure that the logic inside the nodes he uses avoids
15 | * endlessly backpropagated loops
16 | */
17 | public bool allow_recursion { get; set; default = false; }
18 |
19 | public Gtk.Widget menu_content { get { return _menu_content; } set { _menu_content = value; menu.set_child(value); } }
20 | public int grid_size { get { return _grid_size; } set { if (value < 1) _grid_size = 1; else _grid_size = value; } }
21 | /**
22 | * The current extents of the temporary connector
23 | * if null, there is no temporary connector drawn at the moment
24 | */
25 | private Gdk.Rectangle? temp_connector = null;
26 | /**
27 | * The socket that the temporary connector will be attched to
28 | */
29 | private Socket? temp_connected_socket = null;
30 | /**
31 | * The socket that was clicked to invoke the temporary connector
32 | */
33 | private Socket? clicked_socket = null;
34 | /**
35 | * Widget that used to draw selection
36 | */
37 | public Rubberband? rubberband;
38 | /**
39 | * The node that is being moved right now via mouse drag.
40 | * The node that receives the button press event registers
41 | * itself with this property
42 | */
43 | internal NodeRenderer? move_node { get; set; default = null; }
44 | internal NodeRenderer? resize_node { get; set; default = null; }
45 |
46 | public ConnectionRenderer renderer = new ConnectionRenderer();
47 |
48 | static construct {
49 | set_css_name("node-view");
50 | }
51 |
52 | construct {
53 | new CssLoader().ensure();
54 |
55 | set_layout_manager(new NodeViewLayoutManager());
56 | }
57 |
58 | /**
59 | * Instantiate a new NodeView
60 | */
61 | public NodeView() {
62 | menu = new Gtk.Popover();
63 | menu.set_parent(this);
64 | menu.set_has_arrow(false);
65 | }
66 |
67 | /**
68 | * {@inheritDoc}
69 | */
70 | public override void dispose() {
71 | var child = get_first_child();
72 |
73 | while (child != null) {
74 | var delchild = child;
75 | child = child.get_next_sibling();
76 | delchild.unparent();
77 | }
78 |
79 | base.dispose();
80 | }
81 |
82 | public NodeViewLayoutChild get_layout(Gtk.Widget widget) {
83 | return (NodeViewLayoutChild) layout_manager.get_layout_child(widget);
84 | }
85 |
86 | public List get_nodes() {
87 | var result = new List();
88 |
89 | for (var child = get_first_child(); child != null; child = child.get_next_sibling()) {
90 | if (!(child is NodeRenderer))
91 | continue;
92 |
93 | result.append(child as NodeRenderer);
94 | }
95 |
96 | return result;
97 | }
98 |
99 | public List get_selected_nodes() {
100 | var result = new List();
101 |
102 | foreach (var node in get_nodes()) {
103 | if (node.selected)
104 | result.append(node);
105 | }
106 |
107 | return result;
108 | }
109 |
110 | private int round_to_multiply(int number, int multiply) {
111 | return (int) (multiply * Math.round(number / multiply));
112 | }
113 |
114 | [GtkCallback]
115 | private void start_drag(double x, double y) {
116 | if (pick(x, y, Gtk.PickFlags.DEFAULT) == this) {
117 | rubberband?.unparent();
118 | rubberband = new Rubberband(this, x, y);
119 | }
120 | }
121 |
122 | [GtkCallback]
123 | private void process_drag(double offset_x, double offset_y) {
124 | double start_x, start_y, x, y;
125 | drag.get_start_point(out start_x, out start_y);
126 | x = start_x + offset_x;
127 | y = start_y + offset_y;
128 |
129 | if (move_node != null) {
130 | int old_x = move_node.x;
131 | int old_y = move_node.y;
132 |
133 | int new_x = ((int) x - (int) move_node.click_offset_x);
134 | int new_y = ((int) y - (int) move_node.click_offset_y);
135 |
136 | if (old_x == new_x && old_y == new_y)
137 | return;
138 |
139 | if (new_x < 0) new_x = 0;
140 | if (new_y < 0) new_y = 0;
141 |
142 | move_node.x = _grid_size == 1 ? new_x : round_to_multiply(new_x, _grid_size);
143 | move_node.y = _grid_size == 1 ? new_y : round_to_multiply(new_y, _grid_size);
144 |
145 | if (move_node.selected) {
146 | foreach (NodeRenderer node in get_selected_nodes()) {
147 | if (node == move_node) continue;
148 |
149 | node.x -= old_x - move_node.x;
150 | node.y -= old_y - move_node.y;
151 | }
152 | }
153 |
154 | queue_allocate();
155 | }
156 |
157 | if (resize_node != null) {
158 | int d_x, d_y;
159 | Graphene.Rect node_bounds;
160 |
161 | resize_node.compute_bounds(this, out node_bounds);
162 |
163 | d_x = (int) (x - resize_node.click_offset_x - node_bounds.origin.x);
164 | d_y = (int) (y - resize_node.click_offset_y - node_bounds.origin.y);
165 |
166 | int new_width = (int) resize_node.resize_start_width + d_x;
167 | int new_height = (int) resize_node.resize_start_height + d_y;
168 |
169 | if (new_width < 0) new_width = 0;
170 | if (new_height < 0) new_height = 0;
171 |
172 | resize_node.set_size_request(new_width, new_height);
173 | }
174 |
175 | if (temp_connector != null) {
176 | temp_connector.width = (int) (x - temp_connector.x);
177 | temp_connector.height = (int) (y - temp_connector.y);
178 |
179 | queue_draw();
180 | }
181 |
182 | if (rubberband != null) {
183 | rubberband.process_motion(offset_x, offset_y);
184 |
185 | foreach (var node in get_nodes()) {
186 | Graphene.Rect node_bounds, rubberband_bounds, result;
187 |
188 | node.compute_bounds(this, out node_bounds);
189 | rubberband.compute_bounds(this, out rubberband_bounds);
190 | node_bounds.intersection(rubberband_bounds, out result);
191 | node.selected = result.size.width > 0 && result.size.height > 0;
192 | }
193 | }
194 | }
195 |
196 | [GtkCallback]
197 | private void stop_drag(double offset_x, double offset_y) {
198 | double start_x, start_y, x, y;
199 | drag.get_start_point(out start_x, out start_y);
200 | x = start_x + offset_x;
201 | y = start_y + offset_y;
202 |
203 | if (temp_connector != null) {
204 | var widget = pick(x, y, Gtk.PickFlags.DEFAULT);
205 |
206 | if (widget is Socket) {
207 | var socket = (Socket) widget;
208 |
209 | // Relink Sinks
210 | if (
211 | socket is Sink && clicked_socket != null &&
212 | clicked_socket is Sink &&
213 | temp_connected_socket is Source
214 | ) {
215 | if (is_suitable_target(socket, temp_connected_socket)) {
216 | clicked_socket.unlink(temp_connected_socket);
217 | socket.link(temp_connected_socket);
218 | } else
219 | temp_connector = null;
220 |
221 | // Link Sockets
222 | } else if (
223 | socket is Source && temp_connected_socket is Sink ||
224 | socket is Sink && temp_connected_socket is Source
225 | ) {
226 | if (is_suitable_target(socket, temp_connected_socket))
227 | socket.link(temp_connected_socket);
228 | else
229 | temp_connector = null;
230 | }
231 | socket.queue_draw();
232 | } else {
233 | if (
234 | temp_connected_socket is Source &&
235 | clicked_socket != null &&
236 | clicked_socket is Sink
237 | ) {
238 | clicked_socket.unlink(temp_connected_socket);
239 | }
240 | }
241 |
242 | clicked_socket = null;
243 | temp_connected_socket = null;
244 | temp_connector = null;
245 |
246 | queue_draw();
247 | }
248 |
249 | move_node = null;
250 | resize_node = null;
251 | rubberband?.unparent();
252 | rubberband = null;
253 |
254 | queue_resize();
255 | queue_allocate();
256 | }
257 |
258 | [GtkCallback]
259 | private void open_menu(int n_clicks, double x, double y) {
260 | menu.set_pointing_to({ (int) x, (int) y, 1, 1 });
261 | menu.popup();
262 | }
263 |
264 | internal void start_temp_connector(Socket socket) {
265 | clicked_socket = socket;
266 | if (socket is Sink && socket.is_linked()) {
267 | var sink = (Sink) socket;
268 |
269 | temp_connected_socket = sink.sources.last().nth_data(0);
270 | } else {
271 | temp_connected_socket = socket;
272 | }
273 |
274 | Graphene.Point point;
275 | temp_connected_socket.compute_point(this, { 8, 8 }, out point);
276 |
277 | temp_connector = { (int) point.x, (int) point.y, 0, 0 };
278 | }
279 |
280 | /**
281 | * Add a node to this nodeview
282 | */
283 | public void add(NodeRenderer node) {
284 | node.set_parent(this);
285 | }
286 |
287 | /**
288 | * Remove a node from this nodeview
289 | */
290 | public void remove(NodeRenderer node) {
291 | node.unlink_all();
292 | node.unparent();
293 | }
294 |
295 | /**
296 | * Determines wheter one socket can be dropped on another
297 | */
298 | private bool is_suitable_target(Socket from, Socket to) {
299 | // Check whether the sockets have the same type
300 | if (!from.has_same_type(to))
301 | return false;
302 | // Check if the target would lead to a recursion
303 | // If yes, return the value of allow_recursion. If this
304 | // value is set to true, it's completely fine to have
305 | // a recursive graph
306 | if (to is Source && from is Sink) {
307 | if (!allow_recursion)
308 | if (
309 | from.node.is_recursive_forward(to.node) ||
310 | to.node.is_recursive_backward(from.node)
311 | ) return false;
312 | }
313 | if (to is Sink && from is Source) {
314 | if (!allow_recursion)
315 | if (
316 | to.node.is_recursive_forward(from.node) ||
317 | from.node.is_recursive_backward(to.node)
318 | ) return false;
319 | }
320 | if (to is Sink && from is Sink) {
321 | Source? s = ((Sink) from).sources.last().nth_data(0);
322 | if (s == null)
323 | return false;
324 | if (!allow_recursion)
325 | if (
326 | to.node.is_recursive_forward(s.node) ||
327 | s.node.is_recursive_backward(to.node)
328 | ) return false;
329 | }
330 | // If the from from-target is a sink, check if the
331 | // to target is either a source which does not belong to the own node
332 | // or if the to target is another sink (this is valid as we can
333 | // move a connection from one sink to another
334 | if (
335 | from is Sink
336 | && ((to is Sink
337 | && to != from)
338 | || (to is Source
339 | && (!to.node.has_socket(from) || allow_recursion)))
340 | ) return true;
341 |
342 | // Check if the from-target is a source. if yes, make sure the
343 | // to-target is a sink and it does not belong to the own node
344 | else if (
345 | from is Source
346 | && to is Sink
347 | && (!to.node.has_socket(from) || allow_recursion)
348 | ) return true;
349 |
350 | return false;
351 | }
352 |
353 | internal signal void draw_minimap();
354 |
355 | protected override void snapshot(Gtk.Snapshot snapshot) {
356 | // Snapshot all childs not including 'Rubberband'
357 | for (var child = get_first_child(); child != null; child = child.get_next_sibling()) {
358 | if (child is Rubberband)
359 | continue;
360 |
361 | snapshot_child(child, snapshot);
362 | }
363 |
364 | foreach (var node in get_nodes()) {
365 | Graphene.Point sink_point, source_point;
366 |
367 | foreach (Sink sink in node.get_sinks()) {
368 |
369 | foreach (Source source in sink.sources) {
370 | if (
371 | temp_connected_socket != null && source == temp_connected_socket
372 | && clicked_socket != null && sink == clicked_socket
373 | ) continue;
374 |
375 | sink.compute_point(this, { 8, 8 }, out sink_point);
376 | source.compute_point(this, { 8, 8 }, out source_point);
377 |
378 | renderer.snapshot_connection(
379 | snapshot,
380 | source, sink,
381 | {
382 | (int) source_point.x, (int) source_point.y,
383 | (int) (sink_point.x - source_point.x), (int) (sink_point.y - source_point.y)
384 | }
385 | );
386 | }
387 | }
388 | }
389 | draw_minimap();
390 | if (temp_connector != null)
391 | renderer.snapshot_temp_connector(snapshot, temp_connected_socket, temp_connector);
392 |
393 | // Snapshot rubberband over all widgets & custom drawing
394 | if (rubberband != null)
395 | snapshot_child(rubberband, snapshot);
396 | }
397 | }
398 |
399 | protected class NodeViewLayoutChild : Gtk.LayoutChild {
400 | public int x = 0;
401 | public int y = 0;
402 |
403 | public NodeViewLayoutChild(Gtk.Widget widget, Gtk.LayoutManager layout_manager) {
404 | Object(child_widget: widget, layout_manager: layout_manager);
405 | }
406 | }
407 |
408 | private class NodeViewLayoutManager : Gtk.LayoutManager {
409 |
410 | protected override Gtk.SizeRequestMode get_request_mode(Gtk.Widget widget) {
411 | return Gtk.SizeRequestMode.CONSTANT_SIZE;
412 | }
413 |
414 | protected override void measure(Gtk.Widget widget, Gtk.Orientation orientation, int for_size, out int min, out int pref, out int min_base, out int pref_base) {
415 | var node_view = widget as NodeView;
416 |
417 | int lower_bound = 0;
418 | int upper_bound = 0;
419 |
420 | foreach (var node in node_view.get_nodes()) {
421 | var layout = (NodeViewLayoutChild) get_layout_child(node);
422 |
423 | switch (orientation) {
424 | case Gtk.Orientation.HORIZONTAL:
425 | if (layout.x < 0)
426 | lower_bound = int.min(layout.x, lower_bound);
427 | else
428 | upper_bound = int.max(layout.x + node.get_width(), upper_bound);
429 |
430 | break;
431 | case Gtk.Orientation.VERTICAL:
432 | if (layout.y < 0)
433 | lower_bound = int.min(layout.y, lower_bound);
434 | else
435 | upper_bound = int.max(layout.y + node.get_height(), upper_bound);
436 |
437 | break;
438 | }
439 | }
440 |
441 | min = upper_bound - lower_bound;
442 | pref = upper_bound - lower_bound;
443 | min_base = -1;
444 | pref_base = -1;
445 | }
446 |
447 | protected override void allocate(Gtk.Widget widget, int height, int width, int baseline) {
448 | for (var child = widget.get_first_child(); child != null; child = child.get_next_sibling()) {
449 | if (child is Gtk.Native)
450 | continue;
451 |
452 | int child_width, child_height, _;
453 |
454 | child.measure(Gtk.Orientation.HORIZONTAL, -1, out child_width, out _, out _, out _);
455 | child.measure(Gtk.Orientation.VERTICAL, -1, out child_height, out _, out _, out _);
456 |
457 | var layout = (NodeViewLayoutChild) get_layout_child(child);
458 |
459 | child.queue_allocate();
460 | child.allocate_size({
461 | layout.x, layout.y,
462 | child_width, child_height
463 | }, -1);
464 | }
465 | }
466 |
467 | public override Gtk.LayoutChild create_layout_child(Gtk.Widget widget, Gtk.Widget for_child) {
468 | return new NodeViewLayoutChild(for_child, this);
469 | }
470 | }
471 | }
472 |
--------------------------------------------------------------------------------
/src/node.vala:
--------------------------------------------------------------------------------
1 | namespace Flow {
2 |
3 | [GtkTemplate (ui = "/me/paladin/libflow/ui/node.ui")]
4 | public class Node : NodeRenderer {
5 | private List sources = new List();
6 | private List sinks = new List();
7 |
8 | private Gtk.Widget _title_widget;
9 | private TitleStyle _title_style = TitleStyle.FLAT;
10 | private Gtk.Widget _content;
11 | private bool _selected;
12 |
13 | [GtkChild]
14 | private unowned Gtk.PopoverMenu menu;
15 | [GtkChild]
16 | private unowned Gtk.Box main_box;
17 | [GtkChild]
18 | private unowned Gtk.Box title_box;
19 | [GtkChild]
20 | private unowned Gtk.Box sink_box;
21 | [GtkChild]
22 | private unowned Gtk.Box source_box;
23 | [GtkChild]
24 | private unowned Gtk.Box content_box;
25 | public override bool selected {
26 | get { return _selected; }
27 | set {
28 | if (value)
29 | set_state_flags(Gtk.StateFlags.SELECTED, false);
30 | else
31 | unset_state_flags(Gtk.StateFlags.SELECTED);
32 |
33 | _selected = value;
34 | }
35 | }
36 | public Gtk.Widget title_widget {
37 | get { return _title_widget; }
38 | set {
39 | if (_title_widget == value)
40 | return;
41 |
42 | _title_widget?.unparent();
43 | _title_widget = value;
44 |
45 | if (_title_widget != null)
46 | title_box.append(_title_widget);
47 | }
48 | }
49 | public TitleStyle title_style {
50 | get { return _title_style; }
51 | set {
52 | _title_style = value;
53 | title_box.css_classes = value.get_css_styles();
54 | }
55 | }
56 | public Gtk.Widget content {
57 | get { return _content; }
58 | set {
59 | if (_content == value)
60 | return;
61 |
62 | content_box.visible = false;
63 | _content?.unparent();
64 | _content = value;
65 |
66 | if (_content != null) {
67 | content_box.visible = true;
68 | content_box.append(_content);
69 | }
70 | }
71 | }
72 |
73 | static construct {
74 | set_css_name("node");
75 | }
76 |
77 | construct {
78 | var action_group = new SimpleActionGroup();
79 | var delete_action = new SimpleAction("delete", null);
80 | delete_action.activate.connect(() => {
81 | @delete();
82 | });
83 | action_group.add_action(delete_action);
84 |
85 | var unlink_action = new SimpleAction("unlink-all", null);
86 | unlink_action.activate.connect(() => {
87 | unlink_all();
88 | });
89 | action_group.add_action(unlink_action);
90 | var test_action = new SimpleAction("test", null);
91 |
92 | insert_action_group("node", action_group);
93 |
94 | set_layout_manager(new Gtk.BinLayout());
95 |
96 | notify["x"].connect(update_position);
97 | notify["y"].connect(update_position);
98 | notify["parent"].connect(update_position);
99 | }
100 |
101 | public void set_label_name(string name, Gtk.Align halign = Gtk.Align.CENTER, bool bold = true) {
102 | var label = new Gtk.Label(name) { halign = halign };
103 |
104 | if (bold)
105 | label.set_markup(@"$name");
106 |
107 | title_widget = label;
108 | }
109 |
110 | public virtual void @delete() {
111 | var node_view = parent as NodeView;
112 |
113 | node_view.remove(this);
114 | }
115 |
116 | /**
117 | * {@inheritDoc}
118 | */
119 | public override void dispose() {
120 | main_box.unparent();
121 | menu.unparent();
122 | base.dispose();
123 | }
124 |
125 | public new void sink_added(Sink sink) {
126 | var box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
127 | box.spacing = 5;
128 |
129 | box.append(sink);
130 | box.append(sink.label);
131 |
132 | sink_box.append(box);
133 | }
134 |
135 | public new void source_added(Source source) {
136 | var box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
137 | box.spacing = 5;
138 |
139 | box.append(source.label);
140 | box.append(source);
141 |
142 | source_box.append(box);
143 | }
144 |
145 | [GtkCallback]
146 | private void begin_drag(double x, double y) {
147 | var picked_widget = pick(x,y, Gtk.PickFlags.NON_TARGETABLE);
148 |
149 | if (picked_widget != this && !can_drag(picked_widget))
150 | return;
151 |
152 | Gdk.Rectangle resize_area = { get_width() - 8, get_height() - 8, 8, 8 };
153 | var node_view = parent as NodeView;
154 |
155 | if (resize_area.contains_point((int) x, (int) y)) {
156 | node_view.resize_node = this;
157 | resize_start_width = get_width();
158 | resize_start_height = get_height();
159 | } else {
160 | node_view.move_node = this;
161 | }
162 |
163 | click_offset_x = x;
164 | click_offset_y = y;
165 | }
166 |
167 | [GtkCallback]
168 | private void open_menu(int n_clicks, double x, double y) {
169 | menu.set_pointing_to({ (int) x, (int) y, 1, 1 });
170 | menu.popup();
171 | }
172 |
173 | /**
174 | * {@inheritDoc}
175 | */
176 | public new void set_parent(Gtk.Widget parent) {
177 | if (!(parent is NodeView)) {
178 | warning("Trying to add a Flow.Node to something that is not a Flow.NodeView!");
179 | return;
180 | }
181 | base.set_parent(parent);
182 | }
183 |
184 | /**
185 | * Add the given {@link Source} to this Node
186 | */
187 | public override void add_source(Source source) {
188 | if (source.node != null)
189 | return; // This Source is already bound
190 |
191 | if (sources.index(source) != -1)
192 | return; // This node already has this source
193 |
194 | sources.append(source);
195 | source.node = this;
196 | source_added(source);
197 | }
198 |
199 | /**
200 | * Add the given {@link Sink} to this Node
201 | */
202 | public override void add_sink(Sink sink) {
203 | if (sink.node != null)
204 | return; // This Sink is already bound
205 |
206 | if (sinks.index(sink) != -1)
207 | return; //This node already has this sink
208 |
209 | sinks.append(sink);
210 | sink.node = this;
211 | sink_added(sink);
212 | }
213 |
214 | /**
215 | * Remove the given {@link Source} from this Node
216 | */
217 | public override void remove_source(Source source) {
218 | if (sources.index(source) == -1)
219 | return; // This node doesn't have this source
220 |
221 | sources.remove(source);
222 | source.node = null;
223 | source_removed(source);
224 | }
225 |
226 | /**
227 | * Remove the given {@link Sink} from this Node
228 | */
229 | public override void remove_sink(Sink sink) {
230 | if (sinks.index(sink) == -1)
231 | return; // This node doesn't have this sink
232 |
233 | sinks.remove(sink);
234 | sink.node = null;
235 | sink_removed(sink);
236 | }
237 |
238 | /**
239 | * Returns true if the given {@link Sink} is one of this Node's sinks.
240 | */
241 | public override bool has_sink(Sink sink) {
242 | return sinks.index(sink) != -1;
243 | }
244 |
245 | /**
246 | * Returns true if the given {@link Source} is one of this Node's sources.
247 | */
248 | public override bool has_source(Source source) {
249 | return sources.index(source) != -1;
250 | }
251 |
252 | /**
253 | * Returns true if the given {@link Socket} is one of this Node's sockets.
254 | */
255 | public override bool has_socket(Socket socket) {
256 | if (socket is Source)
257 | return has_source(socket as Source);
258 | else
259 | return has_sink(socket as Sink);
260 | }
261 |
262 | /**
263 | * Searches this Node's {@link Socket}s for a Socket with the given name.
264 | * If there is any, it will be returned. Else, null will be returned
265 | */
266 | public override Socket? get_socket(string name) {
267 | foreach (Sink s in sinks)
268 | if (s.name == name)
269 | return s;
270 | foreach (Source s in sources)
271 | if (s.name == name)
272 | return s;
273 | return null;
274 | }
275 |
276 | /**
277 | * Returns the sources of this node
278 | */
279 | public override unowned List get_sources() {
280 | return sources;
281 | }
282 |
283 | /**
284 | * Returns the sinks of this node
285 | */
286 | public override unowned List get_sinks() {
287 | return sinks;
288 | }
289 |
290 | /**
291 | * This method checks whether a connection from the given from-Node
292 | * to this Node would lead to a recursion in the direction source -> sink
293 | */
294 | public override bool is_recursive_forward(NodeRenderer from, bool initial = true) {
295 | if (!initial && this == from)
296 | return true;
297 |
298 | foreach (Source source in get_sources())
299 | foreach (Sink sink in source.sinks)
300 | if (sink.node.is_recursive_forward(from, false))
301 | return true;
302 |
303 | return false;
304 | }
305 |
306 | /**
307 | * This method checks whether a connection from the given from-Node
308 | * to this Node would lead to a recursion in the direction sink -> source
309 | */
310 | public override bool is_recursive_backward(NodeRenderer from, bool initial = true) {
311 | if (!initial && this == from)
312 | return true;
313 |
314 | foreach (Sink sink in sinks)
315 | foreach (Source source in sink.sources)
316 | if (source.node.is_recursive_backward(from, false))
317 | return true;
318 |
319 | return false;
320 | }
321 |
322 | /**
323 | * Gets all neighbor nodes that this node is connected to
324 | */
325 | public override List get_neighbors() {
326 | var result = new List();
327 |
328 | foreach (Source source in get_sources())
329 | foreach (Sink sink in source.sinks)
330 | if (sink.node != null && result.index(sink.node) == -1)
331 | result.append(sink.node);
332 |
333 | foreach (Sink sink in get_sinks())
334 | foreach (Source source in sink.sources)
335 | if (source.node != null && result.index(source.node) == -1)
336 | result.append(source.node);
337 |
338 | return result;
339 | }
340 |
341 | /**
342 | * Returns true if the given node is directly connected
343 | * to this node
344 | */
345 | public override bool is_neighbor(NodeRenderer node) {
346 | return get_neighbors().index(node) != -1;
347 | }
348 |
349 | /**
350 | * Disconnect all connections from and to this node
351 | */
352 | public override void unlink_all() {
353 | foreach (Source source in sources)
354 | source.unlink_all();
355 |
356 | foreach (Sink sink in sinks)
357 | sink.unlink_all();
358 | }
359 |
360 | private bool can_drag(Gtk.Widget widget) {
361 | if (!has_gestures(widget)) {
362 | for (var parent = widget.parent; parent != this; parent = parent.parent) {
363 | if (has_gestures(parent)) {
364 | return false;
365 | }
366 | }
367 | return true;
368 | }
369 | return false;
370 | }
371 |
372 | private bool has_gestures(Gtk.Widget widget) {
373 | var list = widget.observe_controllers();
374 | for (int i = 0; i < list.get_n_items(); i++)
375 | if (list.get_item(i) is Gtk.Gesture)
376 | return true;
377 |
378 | return false;
379 | }
380 |
381 | private void update_position() {
382 | var node_view = parent as NodeView;
383 |
384 | if (node_view == null)
385 | return;
386 |
387 | var layout = node_view.get_layout(this);
388 |
389 | layout.x = x;
390 | layout.y = y;
391 | }
392 | }
393 |
394 | public abstract class NodeRenderer : Gtk.Widget {
395 | /**
396 | * Expresses wheter this node is marked via rubberband selection
397 | */
398 | public virtual bool selected { get; set; }
399 | /**
400 | * Determines wheter the user be allowed to remove the node. Otherwise
401 | * the node can only be removed programmatically
402 | */
403 | public bool deletable { get; set; default = true; }
404 | /**
405 | * Determines wheter the user be allowed to remove the node. Otherwise
406 | * the node can only be removed programmatically
407 | */
408 | public bool resizable { get; set; default = true; }
409 | public Gdk.RGBA? highlight_color { get; set; default = null; }
410 | public int x { get; set; default = 0; }
411 | public int y { get; set; default = 0; }
412 | /**
413 | * Click offset: x coordinate
414 | *
415 | * Holds the offset-position relative to the origin of the
416 | * node at which this node has been clicked the last time.
417 | */
418 | public double click_offset_x { get; protected set; default = 0; }
419 | /**
420 | * Click offset: y coordinate
421 | *
422 | * Holds the offset-position relative to the origin of the
423 | * node at which this node has been clicked the last time.
424 | */
425 | public double click_offset_y { get; protected set; default = 0; }
426 |
427 | /**
428 | * Resize start width
429 | *
430 | * Hold the original width of the node when the last resize process
431 | * had been started
432 | */
433 | public double resize_start_width { get; protected set; default = 0; }
434 | /**
435 | * Resize start height
436 | *
437 | * Hold the original height of the node when the last resize process
438 | * had been started
439 | */
440 | public double resize_start_height { get; protected set; default = 0; }
441 | /**
442 | * This signal is being triggered when a {@link Sink} is added to this Node
443 | */
444 | public signal void sink_added(Sink sink);
445 | /**
446 | * This signal is being triggered when a {@link Source} is added to this Node
447 | */
448 | public signal void source_added(Source source);
449 | /**
450 | * This signal is being triggered when a {@link Sink} is removed from this Node
451 | */
452 | public signal void sink_removed(Sink sink);
453 | /**
454 | * This signal is being triggered when a {@link Source} is removed from this Node
455 | */
456 | public signal void source_removed(Source sink);
457 |
458 | /**
459 | * Implementations should destroy all connections of this Node's {@link Sink}s
460 | * and {@link Source}s when this method is executed
461 | */
462 | public abstract void unlink_all();
463 | /**
464 | * Determines whether the given from-{@link Node} can be found if we
465 | * recursively follow all nodes that are connected to this node's {@link Source}s
466 | */
467 | public abstract bool is_recursive_forward(NodeRenderer from, bool initial = false);
468 | /**
469 | * Determines whether the given from-{@link Node} can be found if we
470 | * recursively follow all nodes that are connected to this node's {@link Sink}s
471 | */
472 | public abstract bool is_recursive_backward(NodeRenderer from, bool initial = false);
473 | /**
474 | * Implementations should return the {@link Socket} with the given name if they contain
475 | * any. If not, return null.
476 | */
477 | public abstract Socket? get_socket(string name);
478 | /**
479 | * Implementations should return true if the given {@link Socket} has been
480 | * assigned to this node
481 | */
482 | public abstract bool has_socket(Socket socket);
483 | /**
484 | * Return a {@link GLib.List} of this Node's {@link Source}s
485 | */
486 | public abstract unowned List get_sources();
487 | /**
488 | * Return a {@link GLib.List} of this Node's {@link Sink}s
489 | */
490 | public abstract unowned List get_sinks();
491 | /**
492 | * Returns the Nodes that this Node is connected to
493 | */
494 | public abstract List get_neighbors();
495 | /**
496 | * Returns true if the given node is directly connected to this node
497 | */
498 | public abstract bool is_neighbor(NodeRenderer node);
499 | /**
500 | * Assign a {@link Source} to this Node
501 | */
502 | public abstract void add_source(Source source);
503 | /**
504 | * Remove a {@link Source} from this Node
505 | */
506 | public abstract void remove_source(Source source);
507 | /**
508 | * Return true if the supplied {@link Source} is assigned to this Node
509 | */
510 | public abstract bool has_source(Source source);
511 | /**
512 | * Assign a {@link Sink} to this Node
513 | */
514 | public abstract void add_sink(Sink sink);
515 | /**
516 | * Return true if the supplied {@link Sink} is assigned to this Node
517 | */
518 | public abstract bool has_sink(Sink sink);
519 | /**
520 | * Remove a {@link Sink} from this Node
521 | */
522 | public abstract void remove_sink(Sink sink);
523 | }
524 | }
525 |
--------------------------------------------------------------------------------
/src/renderers/connection-renderer.vala:
--------------------------------------------------------------------------------
1 | namespace Flow {
2 |
3 | public class ConnectionRenderer : Object {
4 |
5 | public virtual void snapshot_temp_connector(Gtk.Snapshot snapshot, Socket socket, Gdk.Rectangle rect) {
6 | snapshot.append_stroke(
7 | build_curve(rect),
8 | new Gsk.Stroke(socket.line_width),
9 | socket.color
10 | );
11 | }
12 |
13 | public virtual void snapshot_connection(Gtk.Snapshot snapshot, Socket start, Socket end, Gdk.Rectangle rect) {
14 | var path = build_curve(rect);
15 | var stroke = new Gsk.Stroke(start.line_width);
16 | Graphene.Rect bounds;
17 | path.get_stroke_bounds(stroke, out bounds);
18 |
19 | snapshot.push_stroke(path, stroke);
20 | snapshot.append_linear_gradient(bounds, { rect.x, rect.y }, {rect.width + rect.x, rect.height + rect.y}, {{0, start.color}, {1, end.color}});
21 | snapshot.pop();
22 | }
23 |
24 | public virtual Gsk.Path build_curve(Gdk.Rectangle rect) {
25 | var builder = new Gsk.PathBuilder();
26 |
27 | builder.move_to(rect.x, rect.y);
28 | if (rect.width > 0)
29 | builder.rel_cubic_to(rect.width / 3, 0, 2 * rect.width / 3, rect.height, rect.width, rect.height);
30 | else
31 | builder.rel_cubic_to(-rect.width / 3, 0, 1.3F * rect.width, rect.height, rect.width, rect.height);
32 |
33 | return builder.to_path();
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/renderers/socket-renderer.vala:
--------------------------------------------------------------------------------
1 | namespace Flow {
2 |
3 | public class SocketRenderer : Object {
4 |
5 | public virtual void snapshot_socket(Gtk.Snapshot snapshot, Socket socket) {
6 |
7 | var cairo = snapshot.append_cairo(Graphene.Rect().init(0, 0, 16, 16));
8 |
9 | cairo.save();
10 |
11 | render_background(cairo, socket);
12 |
13 | cairo.restore();
14 |
15 | if (socket.is_linked()) {
16 | cairo.save();
17 |
18 | render_linked(cairo, socket, socket.color);
19 |
20 | cairo.restore();
21 | }
22 | }
23 |
24 | public virtual void render_background(Cairo.Context cairo, Socket socket) {
25 | cairo.set_source_rgba(0.5f, 0.5f, 0.5f, 0.5f);
26 | cairo.arc(8, 8, 8, 0, 2 * Math.PI);
27 | cairo.fill();
28 | }
29 |
30 | public virtual void render_linked(Cairo.Context cairo, Socket socket, Gdk.RGBA color) {
31 | cairo.set_source_rgba(
32 | color.red,
33 | color.green,
34 | color.blue,
35 | color.alpha
36 | );
37 | cairo.arc(8, 8, 4, 0.0, 2 * Math.PI);
38 | cairo.fill();
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/rubberband.vala:
--------------------------------------------------------------------------------
1 | namespace Flow {
2 |
3 | protected class Rubberband : Gtk.Widget {
4 | private NodeViewLayoutChild layout;
5 |
6 | public int start_x { get; construct set; }
7 | public int start_y { get; construct set; }
8 |
9 | static construct {
10 | set_css_name("rubberband");
11 | }
12 |
13 | public Rubberband(NodeView parent, double x, double y) {
14 | set_parent(parent);
15 |
16 | layout = parent.get_layout(this);
17 | layout.x = start_x = (int) x;
18 | layout.y = start_y = (int) y;
19 | }
20 |
21 | public void process_motion(double x, double y) {
22 | Gdk.Rectangle selection = {
23 | start_x, start_y,
24 | (int) x, (int) y
25 | };
26 |
27 | if (selection.width < 0) {
28 | selection.width *= -1;
29 | selection.x -= selection.width;
30 | }
31 |
32 | if (selection.height < 0) {
33 | selection.height *= -1;
34 | selection.y -= selection.height;
35 | }
36 |
37 | layout.x = selection.x;
38 | layout.y = selection.y;
39 | set_size_request(selection.width, selection.height);
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/sink.vala:
--------------------------------------------------------------------------------
1 | namespace Flow {
2 |
3 | public class Sink : Socket {
4 | private List _sources = new List();
5 | /**
6 | * The {@link Source}s that this Sink is currently connected to
7 | */
8 | public List sources { get { return _sources; } }
9 | /**
10 | * Defines how many sources can be connected to this sink
11 | *
12 | * Setting this variable to a lower value than the current
13 | * amount of connected sources will have no further effects
14 | * than not allowing more connections.
15 | */
16 | public uint max_sources { get; set; default = 1; }
17 |
18 | construct {
19 | add_css_class("sink");
20 | }
21 |
22 | /**
23 | * Creates a new Sink with type of given value {@link GLib.Value}
24 | */
25 | public Sink(Value @value) {
26 | value_type = @value.get_gtype();
27 | }
28 |
29 | /**
30 | * Creates a new Sink with given type {@link GLib.Type}
31 | */
32 | public Sink.with_type(Type type) {
33 | value_type = type;
34 | }
35 |
36 | protected void add_source(Source source) {
37 | if (value_type == source.value_type) {
38 | _sources.append(source);
39 |
40 | changed();
41 | source.changed.connect(do_source_changed);
42 | }
43 | }
44 |
45 | protected void remove_source(Source source) {
46 | if (_sources.index(source) != -1)
47 | _sources.remove(source);
48 |
49 | if (source.is_linked_to(this)) {
50 | source.unlink(this);
51 |
52 | unlinked(source, _sources.length() == 0);
53 | }
54 | }
55 |
56 | /**
57 | * Returns true if this Source is connected to one or more Sinks
58 | */
59 | public override bool is_linked() {
60 | return _sources.length() > 0;
61 | }
62 |
63 | /**
64 | * Returns true if this Source is connected to the given Sink
65 | */
66 | public override bool is_linked_to(Socket socket) {
67 | if (!(socket is Source))
68 | return false;
69 |
70 | return _sources.index((Source) socket) != -1;
71 | }
72 |
73 | /**
74 | * Connect to the given {@link Socket}
75 | */
76 | public override void link(Socket socket) {
77 | if (is_linked_to(socket))
78 | return;
79 |
80 | if (!before_linking(this, socket))
81 | return;
82 |
83 | // Switching
84 | if (_sources.length() + 1 > max_sources && sources.length() > 0)
85 | unlink(sources.nth_data(sources.length() - 1));
86 |
87 | if (socket is Source) {
88 | add_source((Source) socket);
89 | changed();
90 | socket.link(this);
91 |
92 | linked(socket);
93 | }
94 | }
95 |
96 | /**
97 | * Disconnect from the given {@link Socket}
98 | */
99 | public override void unlink(Socket socket) {
100 | if (!is_linked_to(socket))
101 | return;
102 |
103 | if (socket is Source) {
104 | remove_source((Source) socket);
105 | do_source_changed();
106 | socket.unlinked(this, sources.length() == 0);
107 | socket.changed.disconnect(do_source_changed);
108 | changed();
109 | }
110 | }
111 |
112 | /**
113 | * Disconnects the Sink from all {@link Source}s that supply
114 | * it with data.
115 | */
116 | public override void unlink_all() {
117 | // Copy is required to avoid crash
118 | foreach (var source in _sources.copy())
119 | if (source != null)
120 | unlink(source);
121 | }
122 |
123 | private void do_source_changed(Value? source_value = null, string? flow_id = null) {
124 | changed(source_value, flow_id);
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/socket.vala:
--------------------------------------------------------------------------------
1 | namespace Flow {
2 |
3 | public abstract class Socket : Gtk.Widget {
4 | private Gtk.GestureClick click;
5 |
6 | public SocketRenderer renderer { get; set; }
7 | public Gtk.Widget label { get; private set; }
8 | /**
9 | * The name that will be rendered for this socket
10 | */
11 | public string? description { get; set; }
12 | /**
13 | * Color of this socket
14 | */
15 | public Gdk.RGBA color { get; set; default = { 0, 0, 0, 1 }; }
16 | /**
17 | * Line width of this socket
18 | * Be aware that only {@link Source}s dictate the line width of the
19 | * connections in default {@link ConnectionRenderer}. If this Socket holds a {@link Sink} it
20 | * will have no visible effect.
21 | */
22 | public float line_width { get; set; default = 2; }
23 | /**
24 | * Determines whether this socket is highlighted
25 | * this is usually triggered when the mouse hovers over it
26 | */
27 | public bool highlight { get; set; }
28 |
29 | /**
30 | * Determines whether this socket is active
31 | */
32 | public bool active { get; set; }
33 | /**
34 | * A reference to the node this Socket resides in
35 | */
36 | public weak NodeRenderer? node { get; set; }
37 |
38 | /**
39 | * The type that has been set to this socket
40 | */
41 | public Type value_type { get; construct set; }
42 |
43 | static construct {
44 | set_css_name("socket");
45 | }
46 |
47 | construct {
48 | renderer = new SocketRenderer();
49 |
50 | linked.connect(queue_draw);
51 | unlinked.connect(queue_draw);
52 |
53 | label = new Gtk.Label(name);
54 |
55 | valign = Gtk.Align.CENTER;
56 | halign = Gtk.Align.CENTER;
57 |
58 | click = new Gtk.GestureClick();
59 | add_controller(click);
60 | click.pressed.connect(press_button);
61 |
62 | notify["name"].connect(() => {
63 | if (label is Gtk.Label)
64 | ((Gtk.Label) label).set_text(name);
65 | });
66 | }
67 |
68 | /**
69 | * This signal is being triggered, when there is a connection being established
70 | * from or to this Socket.
71 | */
72 | public signal void linked(Socket socket);
73 |
74 | /**
75 | * This signal is being triggered, before a connection is made
76 | * between two sockets. If the implementor returns false, the
77 | * connection is not being made.
78 | * IMPORTANT: Connect to this signal with connect_after
79 | * otherwise this default handler will be called after the
80 | * signal you implemented, thus always returning true,
81 | * rendering your code ineffective.
82 | */
83 | public virtual signal bool before_linking(Socket self, Socket other) {
84 | return true;
85 | }
86 |
87 | /**
88 | * This signal is being triggered, when there is a connection being removed
89 | * from or to this Socket. If this the last connection of the socket, the
90 | * boolean parameter last will be set to true.
91 | */
92 | public signal void unlinked(Socket socket, bool last);
93 |
94 | /**
95 | * Triggers when the value of this socket changes
96 | */
97 | public signal void changed(Value? value = null, string? flow_id = null);
98 |
99 | /**
100 | * Implementations should return true if this socket has at least one
101 | * connection to another socket
102 | */
103 | public abstract bool is_linked();
104 |
105 | /**
106 | * Implementations should return true if this socket is connected
107 | * to the supplied socket
108 | */
109 | public abstract bool is_linked_to(Socket socket);
110 |
111 | /**
112 | * Connect this {@link Socket} to other {@link Socket}
113 | */
114 | public abstract void link(Socket socket);
115 | /**
116 | * Disconnect this {@link Socket} from other {@link Socket}
117 | */
118 | public abstract void unlink(Socket socket);
119 | /**
120 | * Disconnect this {@link Socket} from all {@link Socket}s it is connected to
121 | */
122 | public abstract void unlink_all();
123 |
124 | /**
125 | * Tries to resolve this Socket's value-type to a displayable string
126 | */
127 | public virtual string determine_typestring() {
128 | return value_type.name();
129 | }
130 |
131 | /**
132 | * Returs true if this and the supplied socket have
133 | * same type
134 | */
135 | public virtual bool has_same_type(Socket other) {
136 | return value_type == other.value_type;
137 | }
138 |
139 | private NodeView? get_nodeview() {
140 | var parent = parent;
141 |
142 | while (true) {
143 | if (parent == null)
144 | return null;
145 |
146 | else if (parent is NodeView)
147 | return (NodeView) parent;
148 |
149 | else
150 | parent = parent.parent;
151 | }
152 | }
153 |
154 | protected override void snapshot(Gtk.Snapshot snapshot) {
155 | base.snapshot(snapshot);
156 |
157 | renderer.snapshot_socket(snapshot, this);
158 | }
159 |
160 | private void press_button(int n_clicked, double x, double y) {
161 | var node_view = get_nodeview();
162 |
163 | if (node_view == null) {
164 | warning("Socket could not process button press: no nodeview");
165 | return;
166 | }
167 |
168 | node_view.start_temp_connector(this);
169 | node_view.queue_allocate();
170 | }
171 |
172 | protected override void measure(Gtk.Orientation o, int for_size, out int min, out int pref, out int min_base, out int pref_base) {
173 | min = 16;
174 | pref = 16;
175 | min_base = -1;
176 | pref_base = -1;
177 | }
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/source.vala:
--------------------------------------------------------------------------------
1 | namespace Flow {
2 |
3 | public class Source : Socket {
4 | private List _sinks = new List();
5 | private Value? last_value;
6 | /**
7 | * The {@link Sink}s that this Source is connected to
8 | */
9 | public List sinks { get { return _sinks; } }
10 |
11 | construct {
12 | add_css_class("source");
13 | }
14 |
15 | /**
16 | * Creates a new Source. Supply an arbitrary {@link GLib.Value}.
17 | * This initial value's type will determine this Source's type.
18 | */
19 | public Source(Value @value) {
20 | value_type = @value.get_gtype();
21 | }
22 |
23 | public Source.with_type(Type type) {
24 | value_type = type;
25 | }
26 |
27 | protected void add_sink(Sink sink) {
28 | if (value_type == sink.value_type) {
29 | _sinks.append(sink);
30 |
31 | changed();
32 | }
33 | }
34 |
35 | protected void remove_sink(Sink sink) {
36 | if (_sinks.index(sink) != -1)
37 | _sinks.remove(sink);
38 |
39 | if (sink.is_linked_to(this)) {
40 | sink.unlink(this);
41 |
42 | unlinked(sink, _sinks.length() == 0);
43 | }
44 | }
45 |
46 | /**
47 | * Returns true if this Source is connected to one or more Sinks
48 | */
49 | public override bool is_linked() {
50 | return _sinks.length() > 0;
51 | }
52 |
53 | /**
54 | * Returns true if this Source is connected to the given Sink
55 | */
56 | public override bool is_linked_to(Socket socket) {
57 | if (!(socket is Sink))
58 | return false;
59 |
60 | return _sinks.index((Sink) socket) != -1;
61 | }
62 |
63 | /**
64 | * Connect to the given {@link Socket}
65 | */
66 | public override void link(Socket socket) {
67 | if (!before_linking(this, socket))
68 | return;
69 |
70 | if (socket is Sink) {
71 | if (is_linked_to(socket))
72 | return;
73 |
74 | add_sink((Sink) socket);
75 | socket.link(this);
76 | changed(last_value);
77 |
78 | linked(socket);
79 | }
80 | }
81 |
82 | /**
83 | * Disconnect from the given {@link Socket}
84 | */
85 | public override void unlink(Socket socket) {
86 | if (!is_linked_to(socket))
87 | return;
88 |
89 | if (socket is Sink)
90 | remove_sink((Sink) socket);
91 | }
92 |
93 | /**
94 | * Disconnect from any {@link Socket} that this Source is connected to
95 | */
96 | public override void unlink_all() {
97 | foreach (var sink in _sinks.copy())
98 | if (sink != null)
99 | unlink(sink);
100 | }
101 |
102 | /**
103 | * Set the value of this Source
104 | */
105 | public void set_value(Value? @value, string? flow_id = null) {
106 | if (@value != null && value_type == @value.type()) {
107 | last_value = @value;
108 |
109 | changed(@value, flow_id);
110 | }
111 | }
112 |
113 | /**
114 | * Returns the last value passed throguh this source
115 | */
116 | public new Value? get_last_value() {
117 | return last_value;
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/title-style.vala:
--------------------------------------------------------------------------------
1 | namespace Flow {
2 |
3 | public enum TitleStyle {
4 | FLAT,
5 | SHADOW,
6 | SEPARATOR;
7 |
8 | public string[] get_css_styles() {
9 | switch (this) {
10 | case TitleStyle.FLAT:
11 | return {};
12 | case TitleStyle.SHADOW:
13 | return { "shadow" };
14 | case TitleStyle.SEPARATOR:
15 | return { "separator" };
16 | default:
17 | assert_not_reached();
18 | }
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/tests/meson.build:
--------------------------------------------------------------------------------
1 | unit_tests = {
2 | 'NodeView': files(
3 | 'node-view-test.vala'
4 | )
5 | }
6 |
7 | unit_tests_deps = [
8 | dependency('gtk4'),
9 | lib_dependency
10 | ]
11 |
12 | foreach name, test_sources : unit_tests
13 | test(name, executable(name, sources: test_sources, dependencies: unit_tests_deps))
14 | endforeach
15 |
--------------------------------------------------------------------------------
/tests/node-view-test.vala:
--------------------------------------------------------------------------------
1 | int main(string[] args) {
2 | Gtk.init();
3 | Test.init(ref args);
4 |
5 | Test.add_func("/libflow/node-view/add", () => {
6 | var node_view = new Flow.NodeView();
7 | var node = new Flow.Node();
8 |
9 | node_view.add(node);
10 |
11 | var nodes = node_view.get_nodes();
12 |
13 | assert(nodes.length() == 1);
14 | assert(nodes.nth_data(0) == node);
15 | });
16 |
17 | return Test.run();
18 | }
19 |
--------------------------------------------------------------------------------