I'm really not happy with this. I'm probably going to throw out half this design, so don't read this document as gospel. I might go with something more like HTML tables(I know, puke a little in your mouth, but swallow it and hear me out!). What I have here is rather like the html tables already, but I don't have any cellspacing or cellpadding or anything like that. I'm thinking it might be a good idea to add something like that. It will also need stretch factors, maybe max and min sizes, and who knows what all else. I've gotta write some more serious apps to really work it out.
Maybe if there were negative values in there too, for right align and such. I want to keep it simple, but not so simple that it is stupid.
Hmmm, gah, I'll just come back to it later.
Anyway, that said, here's a poorly written document about a poorly designed system....
There's a function, setControlGeometry, that needs some explanation. This sets the position and size of the control in your windows, but what do the numbers mean?
The function signature is void setControlGeometry(int id, int x, int y, int w, int h);. The numbers are grid positions. What does this mean though?
The DWS doesn't give you control over the size on screen of your window. This is up to the viewer (which should leave it up to the user whenever possible). It doesn't even tell you the window size. Even if it did, it wouldn't be super helpful - there can be DWS viewers on character cell displays as well as graphical displays, so are you talking pixels or cells?
While you could be told and specialize on them, that's added hassle, and it adds lag: you'd have to be told when the user resizes the window, and the display couldn't update without waiting on your answer.
Instead you set relative positions. I'm not explaining this well in words, let's just get into some math and examples.
Suppose you are writing a simple calculator application. It has buttons on screen laid out just like the numpad on a keyboard, except without a numlock key, with a display above the keys. (0,0) is the upper left corner of the window, so your code might look something like this:
auto result = new LineEdit(window);
result.setGeometry(0, 0, 4, 1);
auto divideButton = new PushButton("/", window, {});
divideButton.setGeometry(1, 1, 1, 1);
auto multiplyButton = new PushButton("*", window, {});
multiplyButton.setGeometry(2, 1, 1, 1);
auto minusButton = new PushButton("-", window, {});
minusButton.setGeometry(3, 1, 1, 1);
auto sevenButton = new PushButton("7", window, {});
sevenButton.setGeometry(0, 2, 1, 1);
auto eightButton = new PushButton("8", window, {});
eightButton.setGeometry(1, 2, 1, 1);
auto nineButton = new PushButton("9", window, {});
nineButton.setGeometry(2, 2, 1, 1);
auto plusButton = new PushButton("+", window, {});
plusButton.setGeometry(3, 2, 1, 2);
auto fourButton = new PushButton("4", window, {});
fourButton.setGeometry(0, 3, 1, 1);
auto fiveButton = new PushButton("5", window, {});
fiveButton.setGeometry(1, 3, 1, 1);
auto sixButton = new PushButton("6", window, {});
sixButton.setGeometry(2, 3, 1, 1);
auto oneButton = new PushButton("1", window, {});
oneButton.setGeometry(0, 4, 1, 1);
auto twoButton = new PushButton("2", window, {});
twoButton.setGeometry(1, 4, 1, 1);
auto threeButton = new PushButton("3", window, {});
threeButton.setGeometry(2, 4, 1, 1);
auto enterButton = new PushButton("=", window, {});
enterButton.setGeometry(3, 4, 1, 2);
auto zeroButton = new PushButton("0", window, {});
zeroButton.setGeometry(0, 5, 2, 1);
auto dotButton = new PushButton(".", window, {});
dotButton.setGeometry(2, 5, 1, 1);
Implementation note: how the viewer should handle this: my current implementation doesn't actually do this. It just depends on Qt's built in grid layout class, which gives slightly different results, which I don't love, but it was easier to code.
As a control's geometry it set, it's parent (the window it is in) should keep track of the maximum x+width and y+height positions. The max x and max y are the width and height of the grid - this isn't explicitly set. (Client program: if you want to set a certain width and height, put a spacer widget at that position. The API might even help out, but it doesn't now.)
The grid size and the window size then become a ratio for actual widget size. Let's say the window is on a 80x25 character cell text display, and we're using the above code.
The max-x and max-y are 4, 6. This is the lower right corner of the most lower right widget. 80 / 4 = 20 and 25 / 6 = 4. (Integer division of course, since we can't cut pixels or cells in half.)
Hence, a width and height of a 1x1 cell would be 20x4 character cells. Simply multiply that by grid position and size to get screen coordinates.
What if some things overlap? The latest control created should be on top. You should try to avoid this though, it is silly.
There will be. What is described here is the low level protocol - the client API can wrap it in a much prettier way. I chose the grid for low level though since I'm pretty sure that other layouts can all be implemented in terms of this grid. If this proves to be wrong when I start coding it, I'll revise this design.
Ideally, I'd like to be able to just new a bunch of widgets and have something that basically works without any thought, and then have some kind of helper classes/functions to position things vertically, horizontally, and in a grid as you new them off. Like in Qt. Heck, a GUI designer tool would probably be nice at some point too, but I'm in no rush there. (Actually, I'd be fairly more tempted to just write a parser for Qt Designer's XML and hack up something that way, rather than reinventing the whole thing.)
UPDATE: I've hacked up something that does some basic automatic layout. As you create things, it keeps a little grid in the window's memory remembering what slots are used. It is a bit brute force, but it works for now. The above code is now greatly simplified:
window.gridWidth = 4; // this is new - you say up front how big you want the window to be (optionally -
//it defaults to an automatic vertical layout otherwise). This is NOT sent to the viewer
// - it still works the same way - it is just for the API to help you through the tedium
window.gridHeight = 6;
auto result = new LineEdit(window);
result.setSize(4, 1);
auto numLock = new PushButton("numlock", window, {}); // placeholder, since spacer isn't implemented yet
auto divideButton = new PushButton("/", window, {});
auto multiplyButton = new PushButton("*", window, {});
auto minusButton = new PushButton("-", window, {});
auto sevenButton = new PushButton("7", window, {});
auto eightButton = new PushButton("8", window, {});
auto nineButton = new PushButton("9", window, {});
auto plusButton = new PushButton("+", window, {});
plusButton.setSize(1, 2);
auto fourButton = new PushButton("4", window, {});
auto fiveButton = new PushButton("5", window, {});
auto sixButton = new PushButton("6", window, {});
auto oneButton = new PushButton("1", window, {});
auto twoButton = new PushButton("2", window, {});
auto threeButton = new PushButton("3", window, {});
auto enterButton = new PushButton("=", window, {});
enterButton.setSize(1, 2);
auto zeroButton = new PushButton("0", window, {});
zeroButton.setSize(2, 1);
auto dotButton = new PushButton(".", window, {});
It now becomes fairly trivial to just make it into a loop.
window.gridWidth = 4;
window.gridHeight = 6;
auto result = new LineEdit(window);
result.setSize(4, 1);
auto buttons = ["numlock", "/", "*", "-", "7", "8", "9", "+", "4", "5", "6", "1", "2", "3" ,"=", "0", "."];
PushButton[] buttonWidgets;
foreach(button; buttons) {
auto b = new PushButton(button, window, {});
buttonWidgets ~= b;
if(button == "+" || button == "=")
b.setSize(1, 2);
if(button == "0")
b.setSize(2, 1);
}
And the library handles most the tedius boring details for us. That's not too bad.