472 lines
16 KiB
D
472 lines
16 KiB
D
|
/* 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);
|
||
|
|
||
|
}
|