1 #include <cppurses/widget/layouts/horizontal.hpp>
2
3 #include <cstddef>
4 #include <deque>
5 #include <iterator>
6 #include <vector>
7
8 #include <cppurses/system/events/move_event.hpp>
9 #include <cppurses/system/events/resize_event.hpp>
10 #include <cppurses/system/system.hpp>
11 #include <cppurses/widget/area.hpp>
12 #include <cppurses/widget/border.hpp>
13 #include <cppurses/widget/point.hpp>
14 #include <cppurses/widget/size_policy.hpp>
15 #include <cppurses/widget/widget.hpp>
16
17 namespace cppurses {
18 namespace layout {
19
calculate_widget_sizes()20 std::vector<Layout::Dimensions> Horizontal::calculate_widget_sizes() {
21 std::vector<Dimensions> widgets;
22 std::size_t total_stretch{0};
23 for (const std::unique_ptr<Widget>& c : this->children.get()) {
24 if (c->enabled()) {
25 widgets.emplace_back(Dimensions{c.get(), 0, this->height()});
26 total_stretch += c->width_policy.stretch();
27 }
28 }
29
30 int width_available = this->width();
31
32 // HORIZONTAL
33 // Set Fixed, Minimum and MinimumExpanding to width_hint
34 for (Dimensions& d : widgets) {
35 auto policy = d.widget->width_policy.type();
36 if (policy == Size_policy::Fixed || policy == Size_policy::Minimum ||
37 policy == Size_policy::MinimumExpanding) {
38 d.width = d.widget->width_policy.hint();
39 width_available -= d.width;
40 }
41 }
42 // if (width_available < 0) {
43 // return widgets;
44 // }
45
46 // Set Size_policy::Ignored widgets to their stretch factor width value.
47 for (Dimensions& d : widgets) {
48 if (d.widget->width_policy.type() == Size_policy::Ignored) {
49 const float percent = d.widget->width_policy.stretch() /
50 static_cast<float>(total_stretch);
51 std::size_t width = percent * this->width();
52 if (width < d.widget->width_policy.min_size()) {
53 width = d.widget->width_policy.min_size();
54 } else if (width > d.widget->width_policy.max_size()) {
55 width = d.widget->width_policy.max_size();
56 }
57 d.width = width;
58 width_available -= width;
59 }
60 }
61
62 // Set Maximum, Preferred and Expanding to width_hint
63 for (Dimensions& d : widgets) {
64 auto policy = d.widget->width_policy.type();
65 if (policy == Size_policy::Maximum ||
66 policy == Size_policy::Preferred ||
67 policy == Size_policy::Expanding) {
68 d.width = d.widget->width_policy.hint();
69 width_available -= d.width;
70 }
71 }
72
73 /// DISTRIBUTE SPACE ------------------------------------------------------
74
75 // create vector of size references for below if statements
76 std::vector<Dimensions_reference> widgets_w_refs;
77 widgets_w_refs.reserve(widgets.size());
78 for (Dimensions& d : widgets) {
79 widgets_w_refs.emplace_back(
80 Dimensions_reference{d.widget, &d.width, &d.height});
81 }
82 // If space left, fill in expanding and min_expanding, then if still,
83 // preferred and min
84 if (width_available > 0) {
85 this->distribute_space(widgets_w_refs, width_available);
86 }
87
88 // if negative space left, subtract from max and preferred, then if still
89 // needed, expanding
90 if (width_available < 0) {
91 this->collect_space(widgets_w_refs, width_available);
92 }
93
94 /// DISTRIBUTE SPACE ------------------------------------------------------
95
96 // VERTICAL - repeat the above, but with vertical properties
97 for (Dimensions& d : widgets) {
98 auto policy = d.widget->height_policy.type();
99 if (policy == Size_policy::Fixed) {
100 d.height = d.widget->height_policy.hint();
101 } else if (policy == Size_policy::Ignored ||
102 policy == Size_policy::Preferred ||
103 policy == Size_policy::Expanding) {
104 if (d.height > d.widget->height_policy.max_size()) {
105 d.height = d.widget->height_policy.max_size();
106 } else if (d.height < d.widget->height_policy.min_size()) {
107 d.height = d.widget->height_policy.min_size();
108 }
109 } else if (policy == Size_policy::Maximum) {
110 if (d.height > d.widget->height_policy.hint()) {
111 d.height = d.widget->height_policy.hint();
112 }
113 } else if (policy == Size_policy::Minimum ||
114 policy == Size_policy::MinimumExpanding) {
115 if (d.height > d.widget->height_policy.max_size()) {
116 d.height = d.widget->height_policy.max_size();
117 } else if (d.height < d.widget->height_policy.hint()) {
118 d.height = d.widget->height_policy.hint();
119 }
120 }
121 }
122 return widgets;
123 }
124
125 // void Horizontal::distribute_space(std::vector<Dimensions>& widgets,
126 // int width_left) {}
127
distribute_space(std::vector<Dimensions_reference> widgets,int width_left)128 void Horizontal::distribute_space(std::vector<Dimensions_reference> widgets,
129 int width_left) {
130 // Find total stretch of first group
131 std::size_t total_stretch{0};
132 for (const Dimensions_reference& d : widgets) {
133 auto policy = d.widget->width_policy.type();
134 if (policy == Size_policy::Expanding ||
135 policy == Size_policy::MinimumExpanding) {
136 total_stretch += d.widget->width_policy.stretch();
137 }
138 }
139
140 // Calculate new widths of widgets in new group, if any go over max_width
141 // then assign max value and recurse without that widget in vector.
142 std::deque<std::size_t> width_additions;
143 int index{0};
144 auto to_distribute = width_left;
145 for (const Dimensions_reference& d : widgets) {
146 auto policy = d.widget->width_policy.type();
147 if (policy == Size_policy::Expanding ||
148 policy == Size_policy::MinimumExpanding) {
149 width_additions.push_back((d.widget->width_policy.stretch() /
150 static_cast<double>(total_stretch)) *
151 to_distribute);
152 if ((*d.width + width_additions.back()) >
153 d.widget->width_policy.max_size()) {
154 width_left -= d.widget->width_policy.max_size() - *d.width;
155 *d.width = d.widget->width_policy.max_size();
156 widgets.erase(std::begin(widgets) + index);
157 return distribute_space(widgets, width_left);
158 }
159 }
160 ++index;
161 }
162
163 // If it has gotten this far, no widgets were over space, assign values
164 for (Dimensions_reference& d : widgets) {
165 auto policy = d.widget->width_policy.type();
166 if (policy == Size_policy::Expanding ||
167 policy == Size_policy::MinimumExpanding) {
168 *d.width += width_additions.front();
169 width_left -= width_additions.front();
170 width_additions.pop_front();
171 }
172 }
173
174 // SECOND GROUP - duplicate of above dependent on Policies to work with.
175 // Preferred and Minimum
176 if (width_left == 0) {
177 return;
178 }
179 // Find total stretch
180 total_stretch = 0;
181 for (const Dimensions_reference& d : widgets) {
182 auto policy = d.widget->width_policy.type();
183 if (policy == Size_policy::Preferred ||
184 policy == Size_policy::Minimum || policy == Size_policy::Ignored) {
185 total_stretch += d.widget->width_policy.stretch();
186 }
187 }
188
189 // Calculate new widths of widgets in new group, if any go over max_width
190 // then assign max value and recurse without that widget in vector.
191 width_additions.clear();
192 index = 0;
193 to_distribute = width_left;
194 for (const Dimensions_reference& d : widgets) {
195 auto policy = d.widget->width_policy.type();
196 if (policy == Size_policy::Preferred ||
197 policy == Size_policy::Minimum || policy == Size_policy::Ignored) {
198 width_additions.push_back((d.widget->width_policy.stretch() /
199 static_cast<double>(total_stretch)) *
200 to_distribute);
201 if ((*d.width + width_additions.back()) >
202 d.widget->width_policy.max_size()) {
203 width_left -= d.widget->width_policy.max_size() - *d.width;
204 *d.width = d.widget->width_policy.max_size();
205 widgets.erase(std::begin(widgets) + index);
206 return distribute_space(widgets, width_left);
207 }
208 }
209 ++index;
210 }
211
212 // If it has gotten this far, no widgets were over space, assign values
213 for (Dimensions_reference& d : widgets) {
214 auto policy = d.widget->width_policy.type();
215 if (policy == Size_policy::Preferred ||
216 policy == Size_policy::Minimum || policy == Size_policy::Ignored) {
217 *d.width += width_additions.front();
218 width_left -= width_additions.front();
219 width_additions.pop_front();
220 }
221 }
222
223 if (width_left == 0) {
224 return;
225 }
226 // Rounding error extra
227 // First Group
228 auto width_check{0};
229 do {
230 width_check = width_left;
231 for (Dimensions_reference& d : widgets) {
232 auto policy = d.widget->width_policy.type();
233 if ((policy == Size_policy::Expanding ||
234 policy == Size_policy::MinimumExpanding) &&
235 width_left > 0) {
236 if (*d.width + 1 <= d.widget->width_policy.max_size()) {
237 *d.width += 1;
238 width_left -= 1;
239 }
240 }
241 }
242 } while (width_check != width_left);
243
244 // Second Group
245 do {
246 width_check = width_left;
247 for (Dimensions_reference& d : widgets) {
248 auto policy = d.widget->width_policy.type();
249 if ((policy == Size_policy::Preferred ||
250 policy == Size_policy::Minimum ||
251 policy == Size_policy::Ignored) &&
252 width_left > 0) {
253 if (*d.width + 1 <= d.widget->width_policy.max_size()) {
254 *d.width += 1;
255 width_left -= 1;
256 }
257 }
258 }
259 } while (width_check != width_left);
260 }
261
262 // void Horizontal::collect_space(std::vector<Dimensions>& widgets,
263 // int width_left) {
264 // // Maximum, Preferred, Ignored
265 // // find maximum space that you can take back.
266 // int available_retake_width{0};
267 // for (Dimensions& d : widgets) {
268 // Size_policy::Type policy = d.widget->width_policy.type();
269 // if (policy == Size_policy::Maximum ||
270 // policy == Size_policy::Preferred ||
271 // policy == Size_policy::Ignored) {
272 // available_retake_width += d.width - d.widget->width_policy.min();
273 // }
274 // }
275 // if (available_retake_width + width_left == 0) {
276 // // set everything to its min() in this group and return
277 // for (Dimensions& d : widgets) {
278 // Size_policy::Type policy = d.widget->width_policy.type();
279 // if (policy == Size_policy::Maximum ||
280 // policy == Size_policy::Preferred ||
281 // policy == Size_policy::Ignored) {
282 // d.width = d.widget->width_policy.min();
283 // }
284 // }
285 // return;
286 // }
287 // if (available_retake_width + width_left < 0) {
288 // // set everything to its min() and continue to expanding group
289 // for (Dimensions& d : widgets) {
290 // Size_policy::Type policy = d.widget->width_policy.type();
291 // if (policy == Size_policy::Maximum ||
292 // policy == Size_policy::Preferred ||
293 // policy == Size_policy::Ignored) {
294 // d.width = d.widget->width_policy.min();
295 // }
296 // }
297 // }
298 // // just take away until width_left == 0
299 // if (available_retake_width + width_left > 0) {
300 // // Ignored
301 // for (Dimensions& d : widgets) {
302 // Size_policy::Type policy = d.widget->width_policy.type();
303 // if (policy == Size_policy::Ignored) {
304 // int potential_retake = d.width -
305 // d.widget->width_policy.min();
306 // // give it all
307 // if (potential_retake + width_left < 0) {
308 // d.width = d.widget->width_policy.min();
309 // }
310 // // only give enough and return
311 // if (potential_retake + width_left >= 0) {
312 // d.width += width_left;
313 // return;
314 // }
315 // }
316 // }
317 // // Maximum
318 // for (Dimensions& d : widgets) {
319 // Size_policy::Type policy = d.widget->width_policy.type();
320 // if (policy == Size_policy::Maximum) {
321 // int potential_retake = d.width -
322 // d.widget->width_policy.min();
323 // // give it all
324 // if (potential_retake + width_left < 0) {
325 // d.width = d.widget->width_policy.min();
326 // }
327 // // only give enough and return
328 // if (potential_retake + width_left >= 0) {
329 // d.width += width_left;
330 // return;
331 // }
332 // }
333 // }
334 // // Preferred
335 // for (Dimensions& d : widgets) {
336 // Size_policy::Type policy = d.widget->width_policy.type();
337 // if (policy == Size_policy::Preferred) {
338 // int potential_retake = d.width -
339 // d.widget->width_policy.min();
340 // // give it all
341 // if (potential_retake + width_left < 0) {
342 // d.width = d.widget->width_policy.min();
343 // }
344 // // only give enough and return
345 // if (potential_retake + width_left >= 0) {
346 // d.width += width_left;
347 // return;
348 // }
349 // }
350 // }
351 // }
352
353 // // Expanding
354 // available_retake_width = 0;
355 // for (Dimensions& d : widgets) {
356 // Size_policy::Type policy = d.widget->width_policy.type();
357 // if (policy == Size_policy::Expanding) {
358 // available_retake_width += d.width - d.widget->width_policy.min();
359 // }
360 // }
361 // if (available_retake_width + width_left <= 0) {
362 // // set everything to its min() in this group and return
363 // for (Dimensions& d : widgets) {
364 // Size_policy::Type policy = d.widget->width_policy.type();
365 // if (policy == Size_policy::Expanding) {
366 // d.width = d.widget->width_policy.min();
367 // }
368 // }
369 // return;
370 // }
371 // if (available_retake_width + width_left > 0) {
372 // for (Dimensions& d : widgets) {
373 // Size_policy::Type policy = d.widget->width_policy.type();
374 // if (policy == Size_policy::Expanding) {
375 // int potential_retake = d.width -
376 // d.widget->width_policy.min();
377 // // give it all
378 // if (potential_retake + width_left < 0) {
379 // d.width = d.widget->width_policy.min();
380 // }
381 // // only give enough and return
382 // if (potential_retake + width_left >= 0) {
383 // d.width += width_left;
384 // return;
385 // }
386 // }
387 // }
388 // }
389 // }
390
collect_space(std::vector<Dimensions_reference> widgets,int width_left)391 void Horizontal::collect_space(std::vector<Dimensions_reference> widgets,
392 int width_left) {
393 if (width_left == 0) {
394 return;
395 }
396 // Find total stretch of first group
397 std::size_t total_stretch{0};
398 for (const Dimensions_reference& d : widgets) {
399 auto policy = d.widget->width_policy.type();
400 if (policy == Size_policy::Maximum ||
401 policy == Size_policy::Preferred ||
402 policy == Size_policy::Ignored) {
403 total_stretch += d.widget->width_policy.stretch();
404 }
405 }
406
407 // Find total of inverse of percentages
408 double total_inverse{0};
409 for (const Dimensions_reference& d : widgets) {
410 auto policy = d.widget->width_policy.type();
411 if (policy == Size_policy::Maximum ||
412 policy == Size_policy::Preferred ||
413 policy == Size_policy::Ignored) {
414 total_inverse += 1 / (d.widget->width_policy.stretch() /
415 static_cast<double>(total_stretch));
416 }
417 }
418
419 // Calculate new widths of widgets in new group, if any go under min_width
420 // then assign min value and recurse without that widget in vector.
421 std::deque<std::size_t> width_deductions;
422 int index{0};
423 auto to_collect = width_left;
424 for (const Dimensions_reference& d : widgets) {
425 auto policy = d.widget->width_policy.type();
426 if (policy == Size_policy::Maximum ||
427 policy == Size_policy::Preferred ||
428 policy == Size_policy::Ignored) {
429 width_deductions.push_back(
430 ((1 / (d.widget->width_policy.stretch() /
431 static_cast<double>(total_stretch))) /
432 static_cast<double>(total_inverse)) *
433 (to_collect * -1));
434 if ((*d.width - width_deductions.back()) <
435 d.widget->width_policy.min_size()) {
436 width_left += *d.width - d.widget->width_policy.min_size();
437 *d.width = d.widget->width_policy.min_size();
438 widgets.erase(std::begin(widgets) + index);
439 return collect_space(widgets, width_left);
440 }
441 }
442 ++index;
443 }
444
445 // If it has gotten this far, no widgets were over space, assign calculated
446 // values
447 for (Dimensions_reference& d : widgets) {
448 auto policy = d.widget->width_policy.type();
449 if (policy == Size_policy::Maximum ||
450 policy == Size_policy::Preferred ||
451 policy == Size_policy::Ignored) {
452 if (*d.width >= width_deductions.front()) {
453 *d.width -= width_deductions.front();
454 } else {
455 *d.width = 0;
456 }
457 width_left += width_deductions.front();
458 width_deductions.pop_front();
459 }
460 }
461
462 // SECOND GROUP - duplicate of above dependent on Policies to work with.
463 if (width_left == 0) {
464 return;
465 }
466 // Find total stretch
467 total_stretch = 0;
468 for (const Dimensions_reference& d : widgets) {
469 auto policy = d.widget->width_policy.type();
470 if (policy == Size_policy::Expanding) {
471 total_stretch += d.widget->width_policy.stretch();
472 }
473 }
474
475 // Find total of inverse of percentages
476 total_inverse = 0;
477 for (const Dimensions_reference& d : widgets) {
478 auto policy = d.widget->width_policy.type();
479 if (policy == Size_policy::Expanding) {
480 total_inverse += 1 / (d.widget->width_policy.stretch() /
481 static_cast<double>(total_stretch));
482 }
483 }
484
485 // Calculate new widths of widgets in new group, if any go over max_width
486 // then assign max value and recurse without that widget in vector.
487 width_deductions.clear();
488 index = 0;
489 to_collect = width_left;
490 for (const Dimensions_reference& d : widgets) {
491 auto policy = d.widget->width_policy.type();
492 if (policy == Size_policy::Expanding) {
493 width_deductions.push_back(
494 ((1 / (d.widget->width_policy.stretch() /
495 static_cast<double>(total_stretch))) /
496 static_cast<double>(total_inverse)) *
497 (to_collect * -1));
498 if ((*d.width - width_deductions.back()) <
499 d.widget->width_policy.min_size()) {
500 width_left += *d.width - d.widget->width_policy.min_size();
501 *d.width = d.widget->width_policy.min_size();
502 widgets.erase(std::begin(widgets) + index);
503 return collect_space(widgets, width_left);
504 }
505 }
506 ++index;
507 }
508
509 // If it has gotten this far, no widgets were over space, assign calculated
510 // values
511 for (Dimensions_reference& d : widgets) {
512 auto policy = d.widget->width_policy.type();
513 if (policy == Size_policy::Expanding) {
514 if (*d.width >= width_deductions.front()) {
515 *d.width -= width_deductions.front();
516 } else {
517 *d.width = 0;
518 }
519 width_left += width_deductions.front();
520 width_deductions.pop_front();
521 }
522 }
523 // Change this to distribute the space, it might not be too small
524 if (width_left != 0) {
525 return;
526 }
527 }
528
move_and_resize_children(const std::vector<Dimensions> & dimensions)529 void Horizontal::move_and_resize_children(
530 const std::vector<Dimensions>& dimensions) {
531 const std::size_t parent_x{this->inner_x()};
532 const std::size_t parent_y{this->inner_y()};
533 const std::size_t parent_width{this->width()};
534 const std::size_t parent_height{this->height()};
535 std::size_t x_pos{parent_x};
536 for (const Dimensions& d : dimensions) {
537 if ((x_pos + d.width) > (parent_x + parent_width) ||
538 (parent_y + d.height) > (parent_y + parent_height) ||
539 d.height == 0 || d.width == 0) {
540 d.widget->disable(true, false); // don't send child_polished_events
541 } else {
542 System::post_event<Move_event>(*(d.widget), Point{x_pos, parent_y});
543 System::post_event<Resize_event>(*(d.widget),
544 Area{d.width, d.height});
545 x_pos += d.width;
546 }
547 }
548 }
549
update_geometry()550 void Horizontal::update_geometry() {
551 this->enable(true, false);
552 std::vector<Dimensions> widths{this->calculate_widget_sizes()};
553 this->move_and_resize_children(widths);
554 }
555
556 } // namespace layout
557 } // namespace cppurses
558