cmake-d/samples/minwin_gtk/minwin/layout.d

472 lines
16 KiB
D
Raw Normal View History

/* MinWin Layout management
*
* Defines a LayoutManager interface and some layout managers
*
* Written by Ben Hinkle and released to the public domain, as
* explained at http://creativecommons.org/licenses/publicdomain
* Report comments and bugs at dsource: http://www.dsource.org/projects/minwin
*/
module minwin.layout;
public import minwin.component;
private import minwin.logging;
interface LayoutManager {
// layout children of c. Can reuse cached information from previous
// preferredSize() or layout() calls in which case the layout
// manager should only be used for a single component.
void layout(Component c);
// compute preferred layout size. Does not reuse cached information.
Point preferredSize(Component c);
// clear cache if present
void reset();
}
/***************** Flow Layout *********************/
enum Dir {
Horizontal, // left to right, then right to left
Vertical // top to bottom, then bottom to top
}
class FlowLayout : LayoutManager {
Dir dir;
int flowGap; // gap between items along flow direction
int flowReverse = 1000; // number of items to lay out before
// flipping to the other side
double endGap = 0.0; // gap at ends of layout, values <= 1 are
// percentage of space
int sideGap; // gap between largest item and side
bool sideStretch; // true means items are stretched to fit to side
int prefWidth;
int prefHeight;
this(Dir dir = Dir.Vertical) {
this.dir = dir;
prefWidth = -1;
prefHeight = -1;
}
void layout(Component c) {
Rect r;
if (c.child is null) return;
version(LOG) log.printf("laying out %x\n",c);
c.getLayoutBounds(r);
version(LOG) log.writefln("layout got bounds %d %d %d %d",
r.left,r.top,r.width,r.height);
int width = r.width;
int height = r.height;
if (prefWidth < 0) {
Point s = preferredSize(c);
prefWidth = s.x;
prefHeight = s.y;
}
int actEndGap = cast(int)endGap; // actual end gap in pixels
int actFlowGap = flowGap;
int x = r.left;
int y = r.top;
int left = x;
int top = y;
int bottom = r.bottom;
int right = r.right;
if (dir == Dir.Vertical) {
if (endGap <= 1.0)
actEndGap = cast(int)(endGap*(height - prefHeight)/2);
y += actEndGap;
} else {
if (endGap <= 1.0)
actEndGap = cast(int)(endGap*(width - prefWidth)/2);
x += actEndGap;
}
int n = 0;
bool isVisible = c.visible();
foreach (Component ch; c ) {
int w, h;
version(LOG) log.printf("doing flow layout for child %x\n",ch);
if (n == flowReverse) {
if (dir == Dir.Vertical)
y = top + height - actEndGap;
else
x = left + width - actEndGap;
actFlowGap = -actFlowGap;
}
if (ch.parentOwnsLayout) {
Point s = ch.preferredSize();
w = s.x;
h = s.y;
version(LOG) log.printf(" got flow child size for %x\n",ch);
if (dir == Dir.Vertical) {
if (n != 0 && n < flowReverse) y += flowGap; // what about first item?
if (n >= flowReverse) y -= h;
int x1 = x, w1 = width;
if (!sideStretch && (w < width)) {
x1 = x+(width-w)/2;
w1 = w;
}
r.LTWH(x1,y,w1,h);
if ((r.top > bottom || r.bottom < top) && ch.parentOwnsVisibility) {
version(LOG)log.printf("layout setting visible off on %x %d %d\n",
ch,r.bottom,height);
ch.visible = false;
} else {
version(LOG)log.writefln("about to set bounds on child %s",
ch.classinfo.name);
ch.setBounds(r);
version(LOG)log.printf("layout checking for visible on %x\n",ch);
if (ch.parentOwnsVisibility && isVisible) {
version(LOG)log.printf("layout setting visible on %x\n",ch);
ch.visible = true;
}
ch.layout(false);
}
if (n < flowReverse) y += h;
} else {
if (n < flowReverse) x += flowGap;
if (n >= flowReverse) x -= w;
int y1 = y, h1 = height;
if (!sideStretch && (h < height)) {
y1 = y+(height-h)/2;
h1 = h;
}
r.LTWH(x,y1,w,h1);
if ((r.left > right || r.right < left) && ch.parentOwnsVisibility) {
version(LOG)log.printf("layout setting visible off on %x\n",ch);
ch.visible = false;
} else {
ch.setBounds(r);
if (ch.parentOwnsVisibility && isVisible)
ch.visible = true;
ch.layout(false);
}
if (n < flowReverse) x += w;
}
}
++n;
}
version(LOG) log.printf(" done laying out %x\n",c);
}
Point preferredSize(Component c) {
version(LOG) log.printf("preferred size for %x\n",c);
int width = 0;
int height = 0;
foreach (Component ch; c ) {
int w, h;
if (ch.parentOwnsLayout) {
Point s = ch.preferredSize;
w = s.x;
h = s.y;
if (dir == Dir.Vertical) {
height += flowGap;
width = width < w ? w : width;
height += h;
} else {
width += flowGap;
height = height < h ? h : height;
width += w;
}
}
}
if (dir == Dir.Vertical) {
height += 2*cast(int)endGap;
width += 2*sideGap;
} else {
width += 2*cast(int)endGap;
height += 2*sideGap;
}
prefWidth = width;
prefHeight = height;
version(LOG) log.printf(" done preferred size for %x %d %d\n",c,width,height);
return XY(width,height);
}
void reset() {
prefWidth = -1;
prefHeight = -1;
}
}
/***************** Table Layout *********************/
// A TableLayout first computes the sum of all the preferred
// sizes of the rows and columns and then divides up the remaining
// space according to the user-supplied distribution. If the
// preferred sizes are greater then the available space the
// preferred sizes are shrunk by the user-supplied distribution to fit
class TableLayout : LayoutManager {
double[] rowScales;
double[] colScales;
int gap;
bool cache;
double[] prows; // preferred row heights
double[] pcols; // preferred column widths
double[] arows; // actual row heights
double[] acols; // actual column widths
// constructs a TableLayout with given scales. The scale
// arrays are duplicated.
this(double[] colScales, double[] rowScales, int gap = 0) {
this.rowScales = rowScales.dup;
this.colScales = colScales.dup;
this.gap = gap;
reset();
}
void reset() {
prows.length = rowScales.length;
pcols.length = colScales.length;
prows[] = 0;
pcols[] = 0;
arows.length = rowScales.length;
acols.length = colScales.length;
cache = false;
}
void layout(Component comp) {
int r,c;
int pref_width, pref_height;
if (comp.child is null) return;
version(LOG) log.printf("laying out %x\n",comp);
if (!cache)
fillPreferredData(comp);
computePreferredSize(comp,pref_width,pref_height);
Rect b,pb;
version(LOG) log.printf("about to get bounds of comp %x\n",comp);
comp.getLayoutBounds(pb);
version(LOG) log.writefln("layout got bounds %d %d %d %d",
pb.left,pb.top,pb.width,pb.height);
int width = pb.width;
int height = pb.height;
// distribute extra space or trim to fit in given space
int extra_width = width - pref_width;
int extra_height = height - pref_height;
version(LOG) log.writefln("extra %d %d \n",extra_width,extra_height);
foreach(int n, double x; prows) {
arows[n] = x + rowScales[n]*extra_height;
version(LOG) log.writefln("rows[n] %g",arows[n]);
}
double total = 0;
foreach(int n, double x; arows) {
total += x;
arows[n] = cast(int)total;
}
foreach(int n, double x; pcols) {
acols[n] = x + colScales[n]*extra_width;
version(LOG) log.writefln("cols[n] %g",acols[n]);
}
total = 0;
foreach(int n, double x; acols) {
total += x;
acols[n] = cast(int)total;
}
int x,y;
c = 0;
r = 0;
foreach (Component ch; comp ) {
if (ch.parentOwnsLayout) {
int nx = cast(int)acols[c];
int ny = cast(int)arows[r];
version(LOG) log.writefln("nx ny %d %d",nx,ny);
b.LTRB(pb.left+x,pb.top+y,nx,ny);
x = nx;
version(LOG) log.printf("setting bounds of %x during layout for %x\n",ch,comp);
ch.setBounds(b);
version(LOG) log.printf("child layout %x during layout for %x\n",ch,comp);
ch.layout(false);
if (++c >= colScales.length) {
c = 0;
x = 0;
y = ny;
if (++r >= rowScales.length)
break; // just stop if too many children
}
}
}
version(LOG) log.printf(" done laying out %x\n",comp);
}
Point preferredSize(Component comp) {
int width,height;
version(LOG) log.printf("preferred size for %x\n",comp);
fillPreferredData(comp);
computePreferredSize(comp,width,height);
version(LOG) log.printf(" done preferred size for %x\n",comp);
return XY(width,height);
}
private void fillPreferredData(Component comp) {
int r,c;
prows[] = 0;
pcols[] = 0;
foreach (Component ch; comp ) {
int w, h;
if (ch.parentOwnsLayout) {
Point s = ch.preferredSize;
w = s.x;
h = s.y;
version(LOG) log.writefln("pref size %d %d",w,h);
prows[r] = prows[r]>h ? prows[r] : h;
pcols[c] = pcols[c]>w ? pcols[c] : w;
version(LOG) log.writefln("row col %g %g",prows[r],pcols[c]);
if (++c >= colScales.length) {
c = 0;
if (++r >= rowScales.length)
break; // just stop if too many children
}
}
}
cache = true;
}
private void computePreferredSize(Component comp,
inout int width,
inout int height) {
double total = 0;
foreach(double x; pcols) { total += x; }
width = cast(int)total;
total = 0;
foreach(double x; prows) { total += x; }
height = cast(int)total;
}
}
/***************** Border Layout *********************/
/*
---------------
| N |
|-------------|
| | | |
|W | C | E|
| | | |
|-------------|
| S |
---------------
*/
enum Loc {
North, South, East, West, Center
}
class BorderLayout : LayoutManager {
Component[Loc.max+1] location; // user fills this in
void layout(Component c) {
Rect r;
if (c.child is null) return;
c.getLayoutBounds(r);
version(LOG) log.writefln("layout got bounds %d %d %d %d",
r.left,r.top,r.width,r.height);
int width = r.width;
int height = r.height;
int west,east,north,south;
int dummy;
if (location[Loc.North])
north = location[Loc.North].preferredSize().y;
if (location[Loc.South])
south = location[Loc.South].preferredSize().y;
if (location[Loc.East])
east = location[Loc.East].preferredSize().x;
if (location[Loc.West])
west = location[Loc.West].preferredSize().x;
Rect cr;
int x0 = r.left;
int y0 = r.top;
// TODO: visible off if not enough space
if (location[Loc.North]) {
cr.LTWH(x0,y0,r.width,north);
location[Loc.North].setBounds(cr);
}
if (location[Loc.South]) {
cr.LTWH(x0,y0+r.height-south,r.width,south);
location[Loc.South].setBounds(cr);
}
if (location[Loc.West]) {
cr.LTWH(x0,y0+north,west,r.height-south-north);
location[Loc.West].setBounds(cr);
}
if (location[Loc.East]) {
cr.LTWH(x0+r.width-east,y0+north,east,r.height-south-north);
location[Loc.East].setBounds(cr);
}
if (location[Loc.Center]) {
cr.LTWH(x0+west,y0+north,width-east-west,height-south-north);
location[Loc.Center].setBounds(cr);
}
}
Point preferredSize(Component c) {
int width = 0;
int height = 0;
int west,east,north,south,centerWidth,centerHeight;
int dummy;
Point center;
Point eastPt;
Point westPt;
if (location[Loc.North])
north = location[Loc.North].preferredSize().y;
if (location[Loc.South])
south = location[Loc.South].preferredSize().y;
if (location[Loc.East])
eastPt = location[Loc.East].preferredSize();
if (location[Loc.West])
westPt = location[Loc.West].preferredSize();
if (location[Loc.Center])
center = location[Loc.Center].preferredSize;
east = eastPt.x;
west = westPt.x;
int maxy = eastPt.y>westPt.y?eastPt.y:westPt.y;
maxy = maxy>center.y?maxy:center.y;
width = east + west + center.x;
height = north + south + maxy;
version(LOG)log.writefln("BorderLayout preferred size %d %d",width,height);
return XY(width,height);
}
void reset() { }
}
unittest {
// simple component for testing layout
class Foo : Component {
Rect bounds;
this(Component parent) {
if (parent)
parent.addChild(this);
}
void disposePeer(){}
PeerForAdd getPeerForAdd() {return parent.getPeerForAdd();}
void getPeerOffset(inout int x, inout int y) {}
void getBounds(inout Rect r) { r = bounds; }
void setBounds(inout Rect r) {
bounds = r;
childLayoutDirty = true;
}
void size(Point s) {
bounds.LTWH(bounds.left,bounds.top,s.x,s.y);
childLayoutDirty = true;
}
void visible(bool vis) { }
bool visible() { return true; }
void repaint() { }
}
Foo a = new Foo(null);
Foo c1 = new Foo(a);
Foo c2 = new Foo(a);
Foo c3 = new Foo(a);
}