lib/zen/src/diagram/ego/layout.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 const root = @import("root.zig");
4
5 const model = root.model;
6
7 pub const margin_width_max: u16 = 256;
8 pub const margin_height_min: u16 = 268;
9 pub const margin_height_max: u16 = 520;
10
11 const wide_leaf_width: u16 = 640;
12 const wide_leaf_height: u16 = 120;
13 const wide_leaf_loop_height: u16 = 152;
14 const wide_leaf_node_width: f32 = 150;
15 const wide_header_y: f32 = 26;
16 const wide_content_top: f32 = 50;
17 const wide_content_bottom: f32 = 408;
18 const vertical_heading_extent: f32 = 18;
19 const vertical_heading_gap: f32 = 16;
20 const vertical_empty_extent: f32 = 32;
21 const vertical_item_gap: f32 = 10;
22 const vertical_flow_gap: f32 = 28;
23
24 pub const Point = struct {
25 x: f32,
26 y: f32,
27 };
28
29 pub const Rect = struct {
30 x: f32,
31 y: f32,
32 width: f32,
33 height: f32,
34
35 pub fn centerX(self: Rect) f32 {
36 return self.x + self.width / 2;
37 }
38
39 pub fn centerY(self: Rect) f32 {
40 return self.y + self.height / 2;
41 }
42 };
43
44 pub const More = struct {
45 count: u32,
46 href: []const u8,
47 };
48
49 pub const Content = union(enum) {
50 node: model.Node,
51 more: More,
52 };
53
54 pub const Item = struct {
55 box: Rect,
56 content: Content,
57 };
58
59 pub const Band = struct {
60 items: [model.max_slots_per_band]Item = undefined,
61 count: u8 = 0,
62 heading: Point,
63 empty: Point,
64
65 pub fn slice(self: *const Band) []const Item {
66 std.debug.assert(self.count <= model.max_slots_per_band);
67 return self.items[0..self.count];
68 }
69 };
70
71 pub const Plan = struct {
72 geometry: model.Geometry,
73 subject: Rect,
74 callers: Band,
75 callees: Band,
76
77 pub fn inspect(network: model.Network, surface: model.Surface) model.Error!Plan {
78 try model.validate(network);
79 const slot_limit = model.slotsPerBand(surface);
80 const callers = try select(network.callers, slot_limit);
81 const callees = try select(network.callees, slot_limit);
82 return switch (surface) {
83 .margin => verticalPlan(network, callers, callees),
84 .wide => horizontalPlan(network, callers, callees),
85 };
86 }
87 };
88
89 const Columns = struct {
90 callers: Rect,
91 subject: Rect,
92 callees: Rect,
93 };
94
95 const Selection = struct {
96 node_count: u8,
97 more_count: u32,
98
99 fn visibleCount(self: Selection) u8 {
100 return self.node_count + @intFromBool(self.more_count > 0);
101 }
102 };
103
104 fn horizontalPlan(
105 network: model.Network,
106 callers: Selection,
107 callees: Selection,
108 ) Plan {
109 const empty = network.callers.total == 0 and network.callees.total == 0;
110 const geometry = wideGeometry(empty, network.recursive);
111 const columns = horizontalColumns(geometry);
112 var plan = Plan{
113 .geometry = geometry,
114 .subject = columns.subject,
115 .callers = horizontalBand(columns.callers, geometry),
116 .callees = horizontalBand(columns.callees, geometry),
117 };
118 placeBand(
119 &plan.callers,
120 network.callers,
121 callers,
122 columns.callers,
123 horizontalFirstY(geometry, callers.visibleCount()),
124 horizontalStep(geometry, callers.visibleCount()),
125 );
126 placeBand(
127 &plan.callees,
128 network.callees,
129 callees,
130 columns.callees,
131 horizontalFirstY(geometry, callees.visibleCount()),
132 horizontalStep(geometry, callees.visibleCount()),
133 );
134 return plan;
135 }
136
137 fn verticalPlan(
138 network: model.Network,
139 callers: Selection,
140 callees: Selection,
141 ) Plan {
142 var geometry = marginGeometry();
143 const center_x = @as(f32, @floatFromInt(geometry.width)) / 2;
144 const callers_extent = verticalBandExtent(geometry, callers.visibleCount());
145 const callees_extent = verticalBandExtent(geometry, callees.visibleCount());
146 const callers_top = geometry.outer_inset + vertical_heading_extent +
147 vertical_heading_gap;
148 const subject_y = callers_top + callers_extent + vertical_flow_gap;
149 const calls_header_top = subject_y + geometry.subject_height + vertical_flow_gap;
150 const callees_top = calls_header_top + vertical_heading_extent +
151 vertical_heading_gap;
152 const height = callees_top + callees_extent + geometry.outer_inset;
153 geometry.height = @intFromFloat(height);
154 std.debug.assert(geometry.height >= margin_height_min);
155 std.debug.assert(geometry.height <= margin_height_max);
156 var plan = Plan{
157 .geometry = geometry,
158 .subject = .{
159 .x = center_x - geometry.subject_width / 2,
160 .y = subject_y,
161 .width = geometry.subject_width,
162 .height = geometry.subject_height,
163 },
164 .callers = verticalBand(geometry, callers_top, geometry.outer_inset),
165 .callees = verticalBand(geometry, callees_top, calls_header_top),
166 };
167 const node_box = Rect{
168 .x = center_x - geometry.node_width / 2,
169 .y = 0,
170 .width = geometry.node_width,
171 .height = geometry.node_height,
172 };
173 const step = geometry.node_height + vertical_item_gap;
174 placeBand(&plan.callers, network.callers, callers, node_box, callers_top, step);
175 placeBand(&plan.callees, network.callees, callees, node_box, callees_top, step);
176 return plan;
177 }
178
179 fn horizontalBand(column: Rect, geometry: model.Geometry) Band {
180 return .{
181 .heading = .{ .x = column.centerX(), .y = wide_header_y },
182 .empty = .{
183 .x = column.centerX(),
184 .y = @as(f32, @floatFromInt(geometry.height)) / 2,
185 },
186 };
187 }
188
189 fn verticalBand(geometry: model.Geometry, content_y: f32, heading_top: f32) Band {
190 const center_x = @as(f32, @floatFromInt(geometry.width)) / 2;
191 return .{
192 .heading = .{ .x = center_x, .y = heading_top + geometry.heading_size },
193 .empty = .{ .x = center_x, .y = content_y + vertical_empty_extent / 2 },
194 };
195 }
196
197 fn placeBand(
198 result: *Band,
199 source: model.Band,
200 selection: Selection,
201 template: Rect,
202 first_y: f32,
203 step: f32,
204 ) void {
205 result.count = selection.visibleCount();
206 std.debug.assert(result.count <= model.max_slots_per_band);
207 var index: u8 = 0;
208 while (index < selection.node_count) : (index += 1) {
209 result.items[index] = .{
210 .box = placedBox(template, first_y, step, index),
211 .content = .{ .node = source.nodes[index] },
212 };
213 }
214 if (selection.more_count == 0) return;
215 result.items[index] = .{
216 .box = placedBox(template, first_y, step, index),
217 .content = .{ .more = .{
218 .count = selection.more_count,
219 .href = source.more_href,
220 } },
221 };
222 }
223
224 fn placedBox(template: Rect, first_y: f32, step: f32, index: u8) Rect {
225 var result = template;
226 result.y = first_y + step * @as(f32, @floatFromInt(index));
227 return result;
228 }
229
230 fn select(source: model.Band, slot_limit: u8) model.Error!Selection {
231 std.debug.assert(slot_limit > 1);
232 std.debug.assert(slot_limit <= model.max_slots_per_band);
233 var node_count: u8 = @intCast(@min(source.nodes.len, slot_limit));
234 if (source.total > node_count and node_count == slot_limit) node_count -= 1;
235 const result = Selection{
236 .node_count = node_count,
237 .more_count = source.total - node_count,
238 };
239 if (result.more_count > 0 and source.more_href.len == 0) {
240 return error.MissingOverflowLink;
241 }
242 return result;
243 }
244
245 fn horizontalColumns(geometry: model.Geometry) Columns {
246 const width: f32 = @floatFromInt(geometry.width);
247 const subject_x = (width - geometry.subject_width) / 2;
248 return .{
249 .callers = .{
250 .x = geometry.outer_inset,
251 .y = 0,
252 .width = geometry.node_width,
253 .height = geometry.node_height,
254 },
255 .subject = .{
256 .x = subject_x,
257 .y = (@as(f32, @floatFromInt(geometry.height)) -
258 geometry.subject_height) / 2,
259 .width = geometry.subject_width,
260 .height = geometry.subject_height,
261 },
262 .callees = .{
263 .x = width - geometry.outer_inset - geometry.node_width,
264 .y = 0,
265 .width = geometry.node_width,
266 .height = geometry.node_height,
267 },
268 };
269 }
270
271 fn horizontalFirstY(geometry: model.Geometry, count: u8) f32 {
272 std.debug.assert(count <= model.wide_slots_per_band);
273 if (count == 1) {
274 return (@as(f32, @floatFromInt(geometry.height)) - geometry.node_height) / 2;
275 }
276 return wide_content_top;
277 }
278
279 fn horizontalStep(geometry: model.Geometry, count: u8) f32 {
280 std.debug.assert(count <= model.wide_slots_per_band);
281 if (count <= 1) return 0;
282 const span = wide_content_bottom - wide_content_top - geometry.node_height;
283 return span / @as(f32, @floatFromInt(count - 1));
284 }
285
286 fn verticalBandExtent(geometry: model.Geometry, count: u8) f32 {
287 std.debug.assert(count <= model.margin_slots_per_band);
288 if (count == 0) return vertical_empty_extent;
289 return geometry.node_height * @as(f32, @floatFromInt(count)) +
290 vertical_item_gap * @as(f32, @floatFromInt(count - 1));
291 }
292
293 fn marginGeometry() model.Geometry {
294 return .{
295 .arrangement = .vertical,
296 .width = margin_width_max,
297 .height = margin_height_min,
298 .slots_per_band = model.margin_slots_per_band,
299 .outer_inset = 8,
300 .node_width = 208,
301 .subject_width = 232,
302 .node_height = 46,
303 .subject_height = 64,
304 .heading_size = 13,
305 .empty_size = 12.5,
306 .label_size = 13,
307 .context_size = 10.5,
308 .badge_size = 8.5,
309 .subject_label_size = 16,
310 };
311 }
312
313 fn wideGeometry(empty: bool, recursive: bool) model.Geometry {
314 const leaf_height = if (recursive) wide_leaf_loop_height else wide_leaf_height;
315 return .{
316 .arrangement = .horizontal,
317 .width = if (empty) wide_leaf_width else 920,
318 .height = if (empty) leaf_height else 420,
319 .slots_per_band = model.wide_slots_per_band,
320 .outer_inset = 20,
321 .node_width = if (empty) wide_leaf_node_width else 240,
322 .subject_width = 260,
323 .node_height = 48,
324 .subject_height = 72,
325 .heading_size = 11,
326 .empty_size = 12,
327 .label_size = 14,
328 .context_size = 10.5,
329 .badge_size = 8.5,
330 .subject_label_size = 18,
331 };
332 }