]> git.tdb.fi Git - libs/gltk.git/blob - source/layout.cpp
Ignore hidden widgets when solving a layout
[libs/gltk.git] / source / layout.cpp
1 #include <algorithm>
2 #include <limits>
3 #include "container.h"
4 #include "layout.h"
5 #include "widget.h"
6
7 using namespace std;
8
9 namespace Msp {
10 namespace GLtk {
11
12 class Layout::LinearProgram
13 {
14 public:
15         class Row
16         {
17         private:
18                 LinearProgram &linprog;
19                 unsigned index;
20
21         public:
22                 Row(LinearProgram &, unsigned);
23
24                 float &operator[](unsigned);
25                 float &back();
26         };
27
28 private:
29         struct Column
30         {
31                 unsigned basic;
32                 std::vector<float> values;
33
34                 Column();
35         };
36
37         unsigned n_columns;
38         unsigned n_rows;
39         std::vector<Column> columns;
40         bool solved;
41         bool infeasible;
42
43 public:
44         LinearProgram(unsigned);
45
46         Row add_row();
47         Row operator[](unsigned);
48         Row get_objective_row();
49         float get_variable(unsigned);
50         bool solve();
51 private:
52         void prepare_columns();
53         void add_artificial_variables();
54         void remove_artificial_variables();
55         unsigned find_minimal_ratio(unsigned);
56         void make_basic_column(unsigned, unsigned);
57         bool pivot();
58 };
59
60
61 struct Layout::Pointers
62 {
63         int Geometry::*pos;
64         unsigned Geometry::*dim;
65         Packing Slot::*packing;
66         unsigned Sides::*low_margin;
67         unsigned Sides::*high_margin;
68         unsigned Layout::*spacing;
69 };
70
71 Layout::Pointers Layout::pointers[2] =
72 { {
73                 &Geometry::x, &Geometry::w,
74                 &Slot::horiz_pack,
75                 &Sides::left, &Sides::right,
76                 &Layout::col_spacing
77         }, {
78                 &Geometry::y, &Geometry::h,
79                 &Slot::vert_pack,
80                 &Sides::bottom, &Sides::top,
81                 &Layout::row_spacing
82 } };
83
84
85 Layout::Layout():
86         container(0),
87         n_active_slots(0),
88         margin(8),
89         row_spacing(5),
90         col_spacing(4)
91 { }
92
93 Layout::~Layout()
94 {
95         for(list<Slot *>::iterator i=slots.begin(); i!=slots.end(); ++i)
96                 delete *i;
97 }
98
99 void Layout::set_container(Container &c)
100 {
101         if(container)
102                 throw logic_error("container!=0");
103
104         container = &c;
105 }
106
107 void Layout::set_margin(const Sides &m)
108 {
109         margin = m;
110         if(container)
111                 update();
112 }
113
114 void Layout::set_spacing(unsigned s)
115 {
116         row_spacing = s;
117         col_spacing = s;
118         if(container)
119                 update();
120 }
121
122 void Layout::set_row_spacing(unsigned s)
123 {
124         row_spacing = s;
125         if(container)
126                 update();
127 }
128
129 void Layout::set_column_spacing(unsigned s)
130 {
131         col_spacing = s;
132         if(container)
133                 update();
134 }
135
136 void Layout::add_widget(Widget &wdg)
137 {
138         if(!container)
139                 throw logic_error("!container");
140
141         Slot *slot = create_slot(wdg);
142         for(list<Constraint>::iterator i=slot->constraints.begin(); i!=slot->constraints.end(); ++i)
143                 i->target.constraints.push_back(Constraint(complement(i->type), *slot));
144         slots.push_back(slot);
145         update_slot_indices();
146         if(container)
147                 update();
148 }
149
150 void Layout::remove_widget(Widget &wdg)
151 {
152         for(list<Slot *>::iterator i=slots.begin(); i!=slots.end(); ++i)
153                 if(&(*i)->widget==&wdg)
154                 {
155                         for(list<Slot *>::iterator j=slots.begin(); j!=slots.end(); ++j)
156                                 if(j!=i)
157                                 {
158                                         for(list<Constraint>::iterator k=(*j)->constraints.begin(); k!=(*j)->constraints.end(); )
159                                         {
160                                                 if(&k->target==*i)
161                                                         (*j)->constraints.erase(k++);
162                                                 else
163                                                         ++k;
164                                         }
165                                 }
166
167                         delete *i;
168                         slots.erase(i);
169
170                         unsigned n = 0;
171                         for(i=slots.begin(); i!=slots.end(); ++i, ++n)
172                                 (*i)->index = n;
173
174                         update();
175                         return;
176                 }
177 }
178
179 Layout::Slot *Layout::create_slot(Widget &wdg)
180 {
181         return new Slot(*this, wdg);
182 }
183
184 void Layout::update_slot_indices()
185 {
186         n_active_slots = 0;
187         for(list<Slot *>::iterator i=slots.begin(); i!=slots.end(); ++i)
188         {
189                 if((*i)->widget.is_visible())
190                         (*i)->index = n_active_slots++;
191                 else
192                         (*i)->index = -1;
193         }
194 }
195
196 Layout::Slot &Layout::get_slot_for_widget(Widget &wdg)
197 {
198         for(list<Slot *>::iterator i=slots.begin(); i!=slots.end(); ++i)
199                 if(&(*i)->widget==&wdg)
200                         return **i;
201
202         throw hierarchy_error("widget not in layout");
203 }
204
205 Layout::ConstraintType Layout::complement(ConstraintType type)
206 {
207         if(type==RIGHT_OF)
208                 return LEFT_OF;
209         else if(type==LEFT_OF)
210                 return RIGHT_OF;
211         else if(type==ABOVE)
212                 return BELOW;
213         else if(type==BELOW)
214                 return ABOVE;
215         else
216                 return type;
217 }
218
219 void Layout::create_constraint(Widget &src, ConstraintType type, Widget &tgt, int sp)
220 {
221         if(&src==&tgt)
222                 throw invalid_argument("&src==&tgt");
223
224         Slot &src_slot = get_slot_for_widget(src);
225         Slot &tgt_slot = get_slot_for_widget(tgt);
226
227         for(list<Constraint>::iterator i=src_slot.constraints.begin(); i!=src_slot.constraints.end(); ++i)
228                 if(i->type==type && &i->target==&tgt_slot)
229                         return;
230
231         src_slot.constraints.push_back(Constraint(type, tgt_slot));
232         src_slot.constraints.back().spacing = sp;
233         tgt_slot.constraints.push_back(Constraint(complement(type), src_slot));
234         tgt_slot.constraints.back().spacing = sp;
235
236         update();
237 }
238
239 void Layout::add_constraint(Widget &src, ConstraintType type, Widget &tgt)
240 {
241         create_constraint(src, type, tgt, -1);
242 }
243
244 void Layout::add_constraint(Widget &src, ConstraintType type, Widget &tgt, unsigned spacing)
245 {
246         create_constraint(src, type, tgt, spacing);
247 }
248
249 void Layout::set_gravity(Widget &wdg, int h, int v)
250 {
251         Slot &slot = get_slot_for_widget(wdg);
252
253         slot.horiz_pack.gravity = h;
254         slot.vert_pack.gravity = v;
255
256         update();
257 }
258
259 void Layout::set_expand(Widget &wdg, bool h, bool v)
260 {
261         Slot &slot = get_slot_for_widget(wdg);
262
263         slot.horiz_pack.expand = h;
264         slot.vert_pack.expand = v;
265
266         update();
267 }
268
269 void Layout::update()
270 {
271         solve_constraints(HORIZONTAL, UPDATE);
272         solve_constraints(VERTICAL, UPDATE);
273
274         for(list<Slot *>::iterator i=slots.begin(); i!=slots.end(); ++i)
275                 (*i)->widget.set_geometry((*i)->geom);
276 }
277
278 void Layout::autosize()
279 {
280         solve_constraints(HORIZONTAL, AUTOSIZE);
281         solve_constraints(VERTICAL, AUTOSIZE);
282
283         container->set_size(autosize_geom.w, autosize_geom.h);
284 }
285
286 void Layout::solve_constraints(int dir, SolveMode mode)
287 {
288         Pointers &ptrs = pointers[dir&VERTICAL];
289
290         const Geometry &geom = container->get_geometry();
291         if(mode==UPDATE && geom.*(ptrs.dim)<margin.*(ptrs.low_margin)+margin.*(ptrs.high_margin))
292                 return;
293
294         /* Set up a linear program to solve the constraints.  The program matrix has
295         five columns for each widget, and one constant column.  The first and second
296         columns of a widget are its position and dimension, respectively.  The
297         remaining three are slack columns; see below for their purposes. */
298         LinearProgram linprog(n_active_slots*5+1);
299         float weight = slots.size();
300         for(list<Slot *>::iterator i=slots.begin(); i!=slots.end(); ++i)
301         {
302                 if((*i)->index<0)
303                         continue;
304
305                 LinearProgram::Row objective = linprog.get_objective_row();
306                 if(mode==AUTOSIZE)
307                 {
308                         objective[(*i)->index*5] = -1;
309                         objective[(*i)->index*5+1] = -1;
310                 }
311                 else
312                 {
313                         objective[(*i)->index*5] = ((*i)->*(ptrs.packing)).gravity/weight;
314                         objective[(*i)->index*5+1] = (((*i)->*(ptrs.packing)).expand ? weight : -1);
315                 }
316
317                 {
318                         // Prevent the widget from going past the container's low edge.
319                         LinearProgram::Row row = linprog.add_row();
320                         row[(*i)->index*5] = 1;
321                         row[(*i)->index*5+2] = -1;
322                         row.back() = margin.*(ptrs.low_margin);
323                 }
324
325                 if(mode==UPDATE)
326                 {
327                         // Prevent the widget from going past the container's high edge.
328                         LinearProgram::Row row = linprog.add_row();
329                         row[(*i)->index*5] = 1;
330                         row[(*i)->index*5+1] = 1;
331                         row[(*i)->index*5+3] = 1;
332                         row.back() = geom.*(ptrs.dim)-margin.*(ptrs.high_margin);
333                 }
334
335                 if(((*i)->*(ptrs.packing)).gravity==0)
336                 {
337                         /* This forces the widget's distance from the left and right edge of
338                         the container to be equal.  It's a bit of a hack, but more time and
339                         thought is needed for a better solution. */
340                         LinearProgram::Row row = linprog.add_row();
341                         row[(*i)->index*5+2] = 1;
342                         row[(*i)->index*5+3] = -1;
343                 }
344
345                 {
346                         /* Only allow the widget's dimension to increase.  The geometry has
347                         previously been set to the smallest allowable size. */
348                         LinearProgram::Row row = linprog.add_row();
349                         row[(*i)->index*5+1] = 1;
350                         row[(*i)->index*5+4] = -1;
351                         row.back() = (*i)->autosize_geom.*(ptrs.dim);
352                 }
353
354                 /* Add rows for user-defined constraints.  Below/above and left/right of
355                 constraints are always added in pairs, so it's only necessary to create a
356                 row for one half. */
357                 for(list<Constraint>::iterator j=(*i)->constraints.begin(); j!=(*i)->constraints.end(); ++j)
358                         if(j->target.index>=0 && (j->type&1)==dir && j->type!=BELOW && j->type!=LEFT_OF)
359                         {
360                                 LinearProgram::Row row = linprog.add_row();
361                                 if(j->type&SELF_POS)
362                                         row[(*i)->index*5] = 1;
363                                 if(j->type&SELF_DIM)
364                                         row[(*i)->index*5+1] = 1;
365                                 if(j->type&TARGET_POS)
366                                         row[j->target.index*5] = -1;
367                                 if(j->type&TARGET_DIM)
368                                         row[j->target.index*5+1] = -1;
369                                 if(j->type&SPACING)
370                                         row.back() = (j->spacing>=0 ? j->spacing : this->*(ptrs.spacing));
371                         }
372         }
373
374         if(!linprog.solve())
375                 return;
376
377         if(mode==AUTOSIZE)
378         {
379                 autosize_geom.*(ptrs.dim) = 0;
380                 for(list<Slot *>::iterator i=slots.begin(); i!=slots.end(); ++i)
381                         if((*i)->index>=0)
382                         {
383                                 int high_edge = linprog.get_variable((*i)->index*5)+linprog.get_variable((*i)->index*5+1);
384                                 autosize_geom.*(ptrs.dim) = max(autosize_geom.*(ptrs.dim), high_edge+margin.*(ptrs.high_margin));
385                         }
386         }
387         else
388         {
389                 for(list<Slot *>::iterator i=slots.begin(); i!=slots.end(); ++i)
390                         if((*i)->index>=0)
391                         {
392                                 (*i)->geom.*(ptrs.pos) = linprog.get_variable((*i)->index*5);
393                                 (*i)->geom.*(ptrs.dim) = linprog.get_variable((*i)->index*5+1);
394                         }
395         }
396 }
397
398
399 Layout::Constraint::Constraint(ConstraintType t, Slot &s):
400         type(t),
401         target(s),
402         spacing(-1)
403 { }
404
405
406 Layout::Packing::Packing():
407         gravity(-1),
408         expand(false)
409 { }
410
411
412 Layout::Slot::Slot(Layout &l, Widget &w):
413         layout(l),
414         index(0),
415         widget(w)
416 {
417         vert_pack.gravity = 1;
418         widget.signal_autosize_changed.connect(sigc::mem_fun(this, &Slot::autosize_changed));
419         widget.signal_visibility_changed.connect(sigc::mem_fun(this, &Slot::visibility_changed));
420         widget.autosize();
421         autosize_geom = widget.get_geometry();
422 }
423
424 void Layout::Slot::autosize_changed()
425 {
426         widget.autosize();
427         autosize_geom = widget.get_geometry();
428
429         if(!widget.is_visible())
430                 return;
431
432         // If the widget fits in the area it had, just leave it there.
433         if(autosize_geom.w<=geom.w && autosize_geom.h<=geom.h)
434                 widget.set_geometry(geom);
435         else
436         {
437                 layout.container->signal_autosize_changed.emit();
438                 layout.update();
439         }
440 }
441
442 void Layout::Slot::visibility_changed(bool v)
443 {
444         layout.update_slot_indices();
445         if(v)
446         {
447                 layout.container->signal_autosize_changed.emit();
448                 layout.update();
449         }
450 }
451
452
453 Layout::LinearProgram::LinearProgram(unsigned s):
454         n_columns(s),
455         n_rows(1),
456         columns(n_columns),
457         solved(false),
458         infeasible(false)
459 { }
460
461 Layout::LinearProgram::Row Layout::LinearProgram::add_row()
462 {
463         return Row(*this, n_rows++);
464 }
465
466 Layout::LinearProgram::Row Layout::LinearProgram::operator[](unsigned r)
467 {
468         if(r>=n_rows)
469                 throw out_of_range("LinearProgram::operator[]");
470
471         return Row(*this, r);
472 }
473
474 Layout::LinearProgram::Row Layout::LinearProgram::get_objective_row()
475 {
476         return Row(*this, 0);
477 }
478
479 float Layout::LinearProgram::get_variable(unsigned i)
480 {
481         if(!solved || infeasible)
482                 throw logic_error("not solved");
483         if(i+1>=n_columns)
484                 throw out_of_range("LinearProgram::get_variable");
485
486         if(unsigned r = columns[i].basic)
487                 return columns.back().values[r];
488         else
489                 return 0;
490 }
491
492 bool Layout::LinearProgram::solve()
493 {
494         if(solved || infeasible)
495                 return !infeasible;
496
497         /* Solve the program using the simplex method.  The column representing the
498         objective variable is kept implicit, as it would never change during the
499         execution of the algorithm. */
500
501         prepare_columns();
502
503         add_artificial_variables();
504
505         // Solve the phase 1 problem.
506         while(pivot()) ;
507
508         /* All artificial variables should now be non-basic and thus zero, so the
509         objective function's value should also be zero.  If it isn't, the original
510         program can't be solved. */
511         if(columns.back().values.front())
512         {
513                 infeasible = true;
514                 return false;
515         }
516
517         remove_artificial_variables();
518
519         // Solve the phase 2 problem.  We already know it to be feasible.
520         while(pivot()) ;
521
522         solved = true;
523
524         return true;
525 }
526
527 void Layout::LinearProgram::prepare_columns()
528 {
529         /* See if any variables are already basic.  A basic variable must only have
530         a nonzero coefficient on one row, and its product with the constant column
531         must not be negative.  Only one variable can be basic for any given row. */
532         vector<float> obj_coeff(n_rows, 0.0f);
533         vector<float> row_coeff(n_rows, 1.0f);
534         const vector<float> &constants = columns.back().values;
535         for(vector<Column>::iterator i=columns.begin(); i!=columns.end(); ++i)
536         {
537                 if(i->values.size()>=2 && i->values.back()!=0.0f && (constants.size()<i->values.size() || i->values.back()*constants[i->values.size()-1]>=0.0f) && obj_coeff[i->values.size()-1]==0.0f)
538                 {
539                         bool basic = true;
540                         for(unsigned j=1; (basic && j+1<i->values.size()); ++j)
541                                 basic = (i->values[j]==0.0f);
542                         if(basic)
543                         {
544                                 i->basic = i->values.size()-1;
545                                 row_coeff[i->basic] = 1.0f/i->values.back();
546                                 obj_coeff[i->basic] = -i->values.front();
547                                 i->values.clear();
548                         }
549                 }
550         }
551
552         // Price out the newly-created basic variables.
553         for(vector<Column>::iterator i=columns.begin(); i!=columns.end(); ++i)
554                 if(!i->values.empty())
555                 {
556                         for(unsigned j=0; j<i->values.size(); ++j)
557                         {
558                                 i->values[j] *= row_coeff[j];
559                                 i->values.front() += obj_coeff[j]*i->values[j];
560                         }
561                 }
562 }
563
564 void Layout::LinearProgram::add_artificial_variables()
565 {
566         vector<unsigned> artificial_rows(n_rows-1);
567         for(unsigned i=0; i<artificial_rows.size(); ++i)
568                 artificial_rows[i] = i+1;
569
570         for(vector<Column>::iterator i=columns.begin(); i!=columns.end(); ++i)
571                 if(i->basic)
572                         artificial_rows[i->basic-1] = 0;
573         artificial_rows.erase(std::remove(artificial_rows.begin(), artificial_rows.end(), 0), artificial_rows.end());
574
575         /* Force all non-basic columns fully into existence and relocate objective
576         row to bottom in preparation of phase 1.  A new objective row is calculated
577         by pricing out the constraint rows. */
578         for(vector<Column>::iterator i=columns.begin(); i!=columns.end(); ++i)
579                 if(!i->basic)
580                 {
581                         float objective = 0.0f;
582                         if(!i->values.empty())
583                         {
584                                 objective = i->values.front();
585                                 i->values.front() = 0.0f;
586                                 for(vector<unsigned>::iterator j=artificial_rows.begin(); j!=artificial_rows.end(); ++j)
587                                         if(*j<i->values.size())
588                                                 i->values.front() += i->values[*j];
589                         }
590                         i->values.resize(n_rows+1, 0.0f);
591                         i->values.back() = objective;
592                 }
593
594         if(artificial_rows.empty())
595                 return;
596
597         /* Create artificial variables for phase 1.  This ensures that each row has
598         a basic variable associated with it.  The original objective row already
599         contains the implicit objective variable, which is basic. */
600         columns.resize(n_columns+artificial_rows.size());
601         columns.back() = columns[n_columns-1];
602         columns[n_columns-1].values.clear();
603         for(unsigned i=0; i<artificial_rows.size(); ++i)
604                 columns[n_columns+i-1].basic = artificial_rows[i];
605 }
606
607 void Layout::LinearProgram::remove_artificial_variables()
608 {
609         /* See if any artificial variables are still basic.  This could be because
610         the program is degenerate.  To avoid trouble later on, use pivots to make
611         some of the original variables basic instead.
612
613         I don't fully understand why this is needed, but it appears to work. */
614         for(unsigned i=n_columns-1; i+1<columns.size(); ++i)
615                 if(columns[i].basic && columns.back().values[columns[i].basic]==0.0f)
616                 {
617                         for(unsigned j=0; j+1<n_columns; ++j)
618                                 if(!columns[j].basic && columns[j].values[columns[i].basic]!=0.0f)
619                                 {
620                                         make_basic_column(j, columns[i].basic);
621                                         break;
622                                 }
623                 }
624
625         /* Get rid of the artificial variables and restore the original objective
626         row to form the phase 2 problem. */
627         columns.erase(columns.begin()+(n_columns-1), columns.end()-1);
628         for(vector<Column>::iterator i=columns.begin(); i!=columns.end(); ++i)
629                 if(!i->basic)
630                 {
631                         i->values.front() = i->values.back();
632                         i->values.pop_back();
633                 }
634 }
635
636 unsigned Layout::LinearProgram::find_minimal_ratio(unsigned c)
637 {
638         /* Pick the row with the minimum ratio between the constant column and the
639         pivot column.  This ensures that when the pivot column is made basic, values
640         in the constant column stay positive.
641
642         The use of n_rows instead of the true size of the column is intentional,
643         since the relocated objective row must be ignored in phase 1. */
644         float best = numeric_limits<float>::infinity();
645         unsigned row = 0;
646         for(unsigned i=1; i<n_rows; ++i)
647                 if(columns[c].values[i]>0)
648                 {
649                         float ratio = columns.back().values[i]/columns[c].values[i];
650                         if(ratio<best)
651                         {
652                                 best = ratio;
653                                 row = i;
654                         }
655                 }
656
657         return row;
658 }
659
660 void Layout::LinearProgram::make_basic_column(unsigned c, unsigned r)
661 {
662         /* Perform row transfer operations to make the pivot column basic,
663         containing a 1 on the pivot row. */
664         for(unsigned i=0; i<columns.size(); ++i)
665                 if(i!=c && (columns[i].basic==r || (!columns[i].basic && columns[i].values[r])))
666                 {
667                         if(columns[i].basic)
668                         {
669                                 columns[i].values.resize(columns.back().values.size(), 0.0f);
670                                 columns[i].values[columns[i].basic] = 1.0f;
671                                 columns[i].basic = 0;
672                         }
673
674                         float scale = columns[i].values[r]/columns[c].values[r];
675
676                         columns[i].values[r] = scale;
677                         for(unsigned j=0; j<columns[i].values.size(); ++j)
678                                 if(j!=r)
679                                         columns[i].values[j] -= scale*columns[c].values[j];
680                 }
681
682         columns[c].basic = r;
683         columns[c].values.clear();
684 }
685
686 bool Layout::LinearProgram::pivot()
687 {
688         /* Pick a nonbasic column and make it basic.  Requiring a positive objective
689         coefficient ensures that the objective function's value will decrease in the
690         process. */
691         for(unsigned i=0; i+1<columns.size(); ++i)
692                 if(!columns[i].basic && columns[i].values.front()>0)
693                         if(unsigned row = find_minimal_ratio(i))
694                         {
695                                 make_basic_column(i, row);
696                                 return true;
697                         }
698
699         return false;
700 }
701
702
703 Layout::LinearProgram::Row::Row(LinearProgram &lp, unsigned i):
704         linprog(lp),
705         index(i)
706 { }
707
708 float &Layout::LinearProgram::Row::operator[](unsigned c)
709 {
710         if(c>=linprog.n_columns)
711                 throw out_of_range("Row::operator[]");
712
713         Column &column = linprog.columns[c];
714         if(column.values.size()<=index)
715                 column.values.resize(index+1);
716
717         return column.values[index];
718 }
719
720 float &Layout::LinearProgram::Row::back()
721 {
722         return (*this)[linprog.n_columns-1];
723 }
724
725
726 Layout::LinearProgram::Column::Column():
727         basic(0)
728 { }
729
730 } // namespace GLtk
731 } // namespace Msp