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