]> git.tdb.fi Git - libs/gltk.git/blob - source/layout.cpp
Smarter way of complementing constraint types
[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         return static_cast<ConstraintType>((type&~(SELF_MASK|TARGET_MASK)) | ((type&SELF_MASK)<<2) | ((type&TARGET_MASK)>>2));
208 }
209
210 void Layout::create_constraint(Widget &src, ConstraintType type, Widget &tgt, int sp)
211 {
212         if(&src==&tgt)
213                 throw invalid_argument("&src==&tgt");
214
215         Slot &src_slot = get_slot_for_widget(src);
216         Slot &tgt_slot = get_slot_for_widget(tgt);
217
218         for(list<Constraint>::iterator i=src_slot.constraints.begin(); i!=src_slot.constraints.end(); ++i)
219                 if(i->type==type && &i->target==&tgt_slot)
220                         return;
221
222         src_slot.constraints.push_back(Constraint(type, tgt_slot));
223         src_slot.constraints.back().spacing = sp;
224         tgt_slot.constraints.push_back(Constraint(complement(type), src_slot));
225         tgt_slot.constraints.back().spacing = sp;
226
227         update();
228 }
229
230 void Layout::add_constraint(Widget &src, ConstraintType type, Widget &tgt)
231 {
232         create_constraint(src, type, tgt, -1);
233 }
234
235 void Layout::add_constraint(Widget &src, ConstraintType type, Widget &tgt, unsigned spacing)
236 {
237         create_constraint(src, type, tgt, spacing);
238 }
239
240 void Layout::set_gravity(Widget &wdg, int h, int v)
241 {
242         Slot &slot = get_slot_for_widget(wdg);
243
244         slot.horiz_pack.gravity = h;
245         slot.vert_pack.gravity = v;
246
247         update();
248 }
249
250 void Layout::set_expand(Widget &wdg, bool h, bool v)
251 {
252         Slot &slot = get_slot_for_widget(wdg);
253
254         slot.horiz_pack.expand = h;
255         slot.vert_pack.expand = v;
256
257         update();
258 }
259
260 void Layout::update()
261 {
262         solve_constraints(HORIZONTAL, UPDATE);
263         solve_constraints(VERTICAL, UPDATE);
264
265         for(list<Slot *>::iterator i=slots.begin(); i!=slots.end(); ++i)
266                 (*i)->widget.set_geometry((*i)->geom);
267 }
268
269 void Layout::autosize()
270 {
271         solve_constraints(HORIZONTAL, AUTOSIZE);
272         solve_constraints(VERTICAL, AUTOSIZE);
273
274         container->set_size(autosize_geom.w, autosize_geom.h);
275 }
276
277 void Layout::solve_constraints(int dir, SolveMode mode)
278 {
279         Pointers &ptrs = pointers[dir&VERTICAL];
280
281         const Geometry &geom = container->get_geometry();
282         if(mode==UPDATE && geom.*(ptrs.dim)<margin.*(ptrs.low_margin)+margin.*(ptrs.high_margin))
283                 return;
284
285         /* Set up a linear program to solve the constraints.  The program matrix has
286         five columns for each widget, and one constant column.  The first and second
287         columns of a widget are its position and dimension, respectively.  The
288         remaining three are slack columns; see below for their purposes. */
289         LinearProgram linprog(n_active_slots*5+1);
290         float weight = slots.size();
291         for(list<Slot *>::iterator i=slots.begin(); i!=slots.end(); ++i)
292         {
293                 if((*i)->index<0)
294                         continue;
295
296                 LinearProgram::Row objective = linprog.get_objective_row();
297                 if(mode==AUTOSIZE)
298                 {
299                         objective[(*i)->index*5] = -1;
300                         objective[(*i)->index*5+1] = -1;
301                 }
302                 else
303                 {
304                         objective[(*i)->index*5] = ((*i)->*(ptrs.packing)).gravity/weight;
305                         objective[(*i)->index*5+1] = (((*i)->*(ptrs.packing)).expand ? weight : -1);
306                 }
307
308                 {
309                         // Prevent the widget from going past the container's low edge.
310                         LinearProgram::Row row = linprog.add_row();
311                         row[(*i)->index*5] = 1;
312                         row[(*i)->index*5+2] = -1;
313                         row.back() = margin.*(ptrs.low_margin);
314                 }
315
316                 if(mode==UPDATE)
317                 {
318                         // Prevent the widget from going past the container's high edge.
319                         LinearProgram::Row row = linprog.add_row();
320                         row[(*i)->index*5] = 1;
321                         row[(*i)->index*5+1] = 1;
322                         row[(*i)->index*5+3] = 1;
323                         row.back() = geom.*(ptrs.dim)-margin.*(ptrs.high_margin);
324                 }
325
326                 if(((*i)->*(ptrs.packing)).gravity==0)
327                 {
328                         /* This forces the widget's distance from the left and right edge of
329                         the container to be equal.  It's a bit of a hack, but more time and
330                         thought is needed for a better solution. */
331                         LinearProgram::Row row = linprog.add_row();
332                         row[(*i)->index*5+2] = 1;
333                         row[(*i)->index*5+3] = -1;
334                 }
335
336                 {
337                         /* Don't allow the widget's dimension to get below that determined
338                         by autosizing. */
339                         LinearProgram::Row row = linprog.add_row();
340                         row[(*i)->index*5+1] = 1;
341                         row[(*i)->index*5+4] = -1;
342                         row.back() = (*i)->autosize_geom.*(ptrs.dim);
343                 }
344
345                 /* Add rows for user-defined constraints.  Constraints are always added
346                 in pairs, so it's only necessary to create a row for one half. */
347                 for(list<Constraint>::iterator j=(*i)->constraints.begin(); j!=(*i)->constraints.end(); ++j)
348                         if(j->target.index>(*i)->index && (j->type&1)==dir)
349                         {
350                                 LinearProgram::Row row = linprog.add_row();
351                                 float polarity = ((j->type&SELF_DIM) ? -1 : 1);
352                                 if(j->type&SELF_POS)
353                                         row[(*i)->index*5] = polarity;
354                                 if(j->type&SELF_DIM)
355                                         row[(*i)->index*5+1] = polarity;
356                                 if(j->type&TARGET_POS)
357                                         row[j->target.index*5] = -polarity;
358                                 if(j->type&TARGET_DIM)
359                                         row[j->target.index*5+1] = -polarity;
360                                 if(j->type&SPACING)
361                                         row.back() = (j->spacing>=0 ? j->spacing : this->*(ptrs.spacing));
362                         }
363         }
364
365         if(!linprog.solve())
366                 return;
367
368         if(mode==AUTOSIZE)
369         {
370                 autosize_geom.*(ptrs.dim) = 0;
371                 for(list<Slot *>::iterator i=slots.begin(); i!=slots.end(); ++i)
372                         if((*i)->index>=0)
373                         {
374                                 int high_edge = linprog.get_variable((*i)->index*5)+linprog.get_variable((*i)->index*5+1);
375                                 autosize_geom.*(ptrs.dim) = max(autosize_geom.*(ptrs.dim), high_edge+margin.*(ptrs.high_margin));
376                         }
377         }
378         else
379         {
380                 for(list<Slot *>::iterator i=slots.begin(); i!=slots.end(); ++i)
381                         if((*i)->index>=0)
382                         {
383                                 (*i)->geom.*(ptrs.pos) = linprog.get_variable((*i)->index*5);
384                                 (*i)->geom.*(ptrs.dim) = linprog.get_variable((*i)->index*5+1);
385                         }
386         }
387 }
388
389
390 Layout::Constraint::Constraint(ConstraintType t, Slot &s):
391         type(t),
392         target(s),
393         spacing(-1)
394 { }
395
396
397 Layout::Packing::Packing():
398         gravity(-1),
399         expand(false)
400 { }
401
402
403 Layout::Slot::Slot(Layout &l, Widget &w):
404         layout(l),
405         index(0),
406         widget(w)
407 {
408         vert_pack.gravity = 1;
409         widget.signal_autosize_changed.connect(sigc::mem_fun(this, &Slot::autosize_changed));
410         widget.signal_visibility_changed.connect(sigc::mem_fun(this, &Slot::visibility_changed));
411         widget.autosize();
412         autosize_geom = widget.get_geometry();
413 }
414
415 void Layout::Slot::autosize_changed()
416 {
417         widget.autosize();
418         autosize_geom = widget.get_geometry();
419
420         if(!widget.is_visible())
421                 return;
422
423         // If the widget fits in the area it had, just leave it there.
424         if(autosize_geom.w<=geom.w && autosize_geom.h<=geom.h)
425                 widget.set_geometry(geom);
426         else
427         {
428                 layout.container->signal_autosize_changed.emit();
429                 layout.update();
430         }
431 }
432
433 void Layout::Slot::visibility_changed(bool v)
434 {
435         layout.update_slot_indices();
436         if(v)
437         {
438                 layout.container->signal_autosize_changed.emit();
439                 layout.update();
440         }
441 }
442
443
444 Layout::LinearProgram::LinearProgram(unsigned s):
445         n_columns(s),
446         n_rows(1),
447         columns(n_columns),
448         solved(false),
449         infeasible(false)
450 { }
451
452 Layout::LinearProgram::Row Layout::LinearProgram::add_row()
453 {
454         return Row(*this, n_rows++);
455 }
456
457 Layout::LinearProgram::Row Layout::LinearProgram::operator[](unsigned r)
458 {
459         if(r>=n_rows)
460                 throw out_of_range("LinearProgram::operator[]");
461
462         return Row(*this, r);
463 }
464
465 Layout::LinearProgram::Row Layout::LinearProgram::get_objective_row()
466 {
467         return Row(*this, 0);
468 }
469
470 float Layout::LinearProgram::get_variable(unsigned i)
471 {
472         if(!solved || infeasible)
473                 throw logic_error("not solved");
474         if(i+1>=n_columns)
475                 throw out_of_range("LinearProgram::get_variable");
476
477         if(unsigned r = columns[i].basic)
478                 return columns.back().values[r];
479         else
480                 return 0;
481 }
482
483 bool Layout::LinearProgram::solve()
484 {
485         if(solved || infeasible)
486                 return !infeasible;
487
488         /* Solve the program using the simplex method.  The column representing the
489         objective variable is kept implicit, as it would never change during the
490         execution of the algorithm. */
491
492         prepare_columns();
493
494         add_artificial_variables();
495
496         // Solve the phase 1 problem.
497         while(pivot()) ;
498
499         /* All artificial variables should now be non-basic and thus zero, so the
500         objective function's value should also be zero.  If it isn't, the original
501         program can't be solved. */
502         if(columns.back().values.front())
503         {
504                 infeasible = true;
505                 return false;
506         }
507
508         remove_artificial_variables();
509
510         // Solve the phase 2 problem.  We already know it to be feasible.
511         while(pivot()) ;
512
513         solved = true;
514
515         return true;
516 }
517
518 void Layout::LinearProgram::prepare_columns()
519 {
520         /* See if any variables are already basic.  A basic variable must only have
521         a nonzero coefficient on one row, and its product with the constant column
522         must not be negative.  Only one variable can be basic for any given row. */
523         vector<float> obj_coeff(n_rows, 0.0f);
524         vector<float> row_coeff(n_rows, 1.0f);
525         const vector<float> &constants = columns.back().values;
526         for(vector<Column>::iterator i=columns.begin(); i!=columns.end(); ++i)
527         {
528                 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)
529                 {
530                         bool basic = true;
531                         for(unsigned j=1; (basic && j+1<i->values.size()); ++j)
532                                 basic = (i->values[j]==0.0f);
533                         if(basic)
534                         {
535                                 i->basic = i->values.size()-1;
536                                 row_coeff[i->basic] = 1.0f/i->values.back();
537                                 obj_coeff[i->basic] = -i->values.front();
538                                 i->values.clear();
539                         }
540                 }
541         }
542
543         // Price out the newly-created basic variables.
544         for(vector<Column>::iterator i=columns.begin(); i!=columns.end(); ++i)
545                 if(!i->values.empty())
546                 {
547                         for(unsigned j=0; j<i->values.size(); ++j)
548                         {
549                                 i->values[j] *= row_coeff[j];
550                                 i->values.front() += obj_coeff[j]*i->values[j];
551                         }
552                 }
553 }
554
555 void Layout::LinearProgram::add_artificial_variables()
556 {
557         vector<unsigned> artificial_rows(n_rows-1);
558         for(unsigned i=0; i<artificial_rows.size(); ++i)
559                 artificial_rows[i] = i+1;
560
561         for(vector<Column>::iterator i=columns.begin(); i!=columns.end(); ++i)
562                 if(i->basic)
563                         artificial_rows[i->basic-1] = 0;
564         artificial_rows.erase(std::remove(artificial_rows.begin(), artificial_rows.end(), 0), artificial_rows.end());
565
566         /* Force all non-basic columns fully into existence and relocate objective
567         row to bottom in preparation of phase 1.  A new objective row is calculated
568         by pricing out the constraint rows. */
569         for(vector<Column>::iterator i=columns.begin(); i!=columns.end(); ++i)
570                 if(!i->basic)
571                 {
572                         float objective = 0.0f;
573                         if(!i->values.empty())
574                         {
575                                 objective = i->values.front();
576                                 i->values.front() = 0.0f;
577                                 for(vector<unsigned>::iterator j=artificial_rows.begin(); j!=artificial_rows.end(); ++j)
578                                         if(*j<i->values.size())
579                                                 i->values.front() += i->values[*j];
580                         }
581                         i->values.resize(n_rows+1, 0.0f);
582                         i->values.back() = objective;
583                 }
584
585         if(artificial_rows.empty())
586                 return;
587
588         /* Create artificial variables for phase 1.  This ensures that each row has
589         a basic variable associated with it.  The original objective row already
590         contains the implicit objective variable, which is basic. */
591         columns.resize(n_columns+artificial_rows.size());
592         columns.back() = columns[n_columns-1];
593         columns[n_columns-1].values.clear();
594         for(unsigned i=0; i<artificial_rows.size(); ++i)
595                 columns[n_columns+i-1].basic = artificial_rows[i];
596 }
597
598 void Layout::LinearProgram::remove_artificial_variables()
599 {
600         /* See if any artificial variables are still basic.  This could be because
601         the program is degenerate.  To avoid trouble later on, use pivots to make
602         some of the original variables basic instead.
603
604         I don't fully understand why this is needed, but it appears to work. */
605         for(unsigned i=n_columns-1; i+1<columns.size(); ++i)
606                 if(columns[i].basic && columns.back().values[columns[i].basic]==0.0f)
607                 {
608                         for(unsigned j=0; j+1<n_columns; ++j)
609                                 if(!columns[j].basic && columns[j].values[columns[i].basic]!=0.0f)
610                                 {
611                                         make_basic_column(j, columns[i].basic);
612                                         break;
613                                 }
614                 }
615
616         /* Get rid of the artificial variables and restore the original objective
617         row to form the phase 2 problem. */
618         columns.erase(columns.begin()+(n_columns-1), columns.end()-1);
619         for(vector<Column>::iterator i=columns.begin(); i!=columns.end(); ++i)
620                 if(!i->basic)
621                 {
622                         i->values.front() = i->values.back();
623                         i->values.pop_back();
624                 }
625 }
626
627 unsigned Layout::LinearProgram::find_minimal_ratio(unsigned c)
628 {
629         /* Pick the row with the minimum ratio between the constant column and the
630         pivot column.  This ensures that when the pivot column is made basic, values
631         in the constant column stay positive.
632
633         The use of n_rows instead of the true size of the column is intentional,
634         since the relocated objective row must be ignored in phase 1. */
635         float best = numeric_limits<float>::infinity();
636         unsigned row = 0;
637         for(unsigned i=1; i<n_rows; ++i)
638                 if(columns[c].values[i]>0)
639                 {
640                         float ratio = columns.back().values[i]/columns[c].values[i];
641                         if(ratio<best)
642                         {
643                                 best = ratio;
644                                 row = i;
645                         }
646                 }
647
648         return row;
649 }
650
651 void Layout::LinearProgram::make_basic_column(unsigned c, unsigned r)
652 {
653         /* Perform row transfer operations to make the pivot column basic,
654         containing a 1 on the pivot row. */
655         for(unsigned i=0; i<columns.size(); ++i)
656                 if(i!=c && (columns[i].basic==r || (!columns[i].basic && columns[i].values[r])))
657                 {
658                         if(columns[i].basic)
659                         {
660                                 columns[i].values.resize(columns.back().values.size(), 0.0f);
661                                 columns[i].values[columns[i].basic] = 1.0f;
662                                 columns[i].basic = 0;
663                         }
664
665                         float scale = columns[i].values[r]/columns[c].values[r];
666
667                         columns[i].values[r] = scale;
668                         for(unsigned j=0; j<columns[i].values.size(); ++j)
669                                 if(j!=r)
670                                         columns[i].values[j] -= scale*columns[c].values[j];
671                 }
672
673         columns[c].basic = r;
674         columns[c].values.clear();
675 }
676
677 bool Layout::LinearProgram::pivot()
678 {
679         /* Pick a nonbasic column and make it basic.  Requiring a positive objective
680         coefficient ensures that the objective function's value will decrease in the
681         process. */
682         for(unsigned i=0; i+1<columns.size(); ++i)
683                 if(!columns[i].basic && columns[i].values.front()>0)
684                         if(unsigned row = find_minimal_ratio(i))
685                         {
686                                 make_basic_column(i, row);
687                                 return true;
688                         }
689
690         return false;
691 }
692
693
694 Layout::LinearProgram::Row::Row(LinearProgram &lp, unsigned i):
695         linprog(lp),
696         index(i)
697 { }
698
699 float &Layout::LinearProgram::Row::operator[](unsigned c)
700 {
701         if(c>=linprog.n_columns)
702                 throw out_of_range("Row::operator[]");
703
704         Column &column = linprog.columns[c];
705         if(column.values.size()<=index)
706                 column.values.resize(index+1);
707
708         return column.values[index];
709 }
710
711 float &Layout::LinearProgram::Row::back()
712 {
713         return (*this)[linprog.n_columns-1];
714 }
715
716
717 Layout::LinearProgram::Column::Column():
718         basic(0)
719 { }
720
721 } // namespace GLtk
722 } // namespace Msp