Archive

Archive for November 6, 2017

ClientRect, BoundsRect and adventures in Smart Pascal layout land

November 6, 2017 Leave a comment

HTML really is the kitchen sink of ideas. Some of them are good, others are bad – but all them have valid reason for being there.

When coming from Delphi or C++ builder to web development you really feel like you have tumbled down the rabbit hole from time to time. Especially when it comes to things like margins, padding and clientrect values.

You would imagine that BoundsRect gives you the full size of a control. In fact BoundsRect() should just be the same as putting left, top, width, height into a TRect structure right? Same with ClientRect, it should be the same as putting 0, 0, ClientWidth, ClientHeight into a TRect structure right?

Smart Mobile Studio uses absolute positioning, which means that you can layout controls at ordinary cartesian coordinate values. If you place a button at position 10, 10 – that means 10 pixels from the left edge and 10 pixels from the top edge. This is what we are used to from Delphi and other native languages.

But the browser have different boxing models, or box-sizing modes if you like. We are using the one best suited for per-pixel-positioning, namely “border-box”. This means that the width and height values for the control will include the size of the content, it’s padding and the size of the border. It excludes things like margin since that is just empty air the browser adds to the final co-ordinates of a visual control.

Doing it by the book

Since we are taking Smart out of the homebrew production style these days, there had to come a time where this must be dealt with.

If you don’t care about making your own controls then this wont effect you at all. You will always be able to drag & drop some controls on the form-designer, or (like most of us does) create them from code and perform layout during the Resize() method.

But .. if you want to make controls that conforms to our theme engine, that actually give a damn about margins, padding and wants give CSS the power to change existing controls the way it deserves, then you better pay attention.

Having experimented with this for a while now, here are the two cardinal rules you must follow if you want your controls to take height for the margin, padding and border-sizes defined in our CSS theme files:

  1. Margins only apply when positioning child elements with margins
  2. When doing layout of child elements, padding only apply from the parent or container of content, not the content itself.

Example for rule #1

Imagine you have a panel on a form. You want to populate that panel with 10 child elements and you want to do it properly, taking height for whatever padding the panel may have – and also whatever margin’s may exist for some child elements.

  var dx := W3Panel1.Border.Left.Padding;
  var dy := W3Panel1.Border.Top.Padding;
  for var x := 0 to 9 do
  begin
    var Item := FItems[x];
    var ItemRect := TRect.Create(dx, dy, Item.width, item. height);
    ItemRect.right -= (Item.Border.Margin.Left + Item.Border.Margin.Right);
    ItemRect.bottom -= (Item.Border.Margin.Top + Item.Border.Margin.Bottom);
    Item.SetBounds(ItemRect);
  end;

Look at the code above. Notice that we dont initialize dx and dy to 0 (zero). We could ofcourse but that would defeat the purpose of being CSS friendsly.

Also notice that we dont add the left and top margin to the final rectangle, this is because the browser automatically does this for us. Instead, we need to scale the right and bottom edge of the rectangle by subtracting the size of the left and right / top and bottom margins.

So if you want theme friendly layout’s, you have to go the extra mile and include these things.

Note: The above was just an example, our ClientRect() function already deals with padding for us, so you would set dx and dy to ClientRect.left and ClientRect.top.

ClientWidth and ClientHeight methods however, remain unaffected by padding. Because there will be cases where you want full control and non-conformity.

Example of rule #2

Think of a text-editor. You want to add a bit of margin to the document and simply drag the left-margin widget to where you need it. Padding for HTML elements works pretty much the same way.

To demonstrate I will create a test container class and a test child class.

First, create a new visual application to play with. Drop a TW3Panel control on the form and size it to full the form (with a bit of air from the edges naturally).

Next, go into project options and check the “use custom stylesheet”. That way Smart will clone whatever style you are using and create a new node in your project manager. Add the following CSS to the stylesheet:

.TTestOwner {
  margin: 20px;
  padding: 4px;
  border: 3px solid #FFFF00;
  background-color: #FF0000;
}

.TTestChild {
  margin: 10px;
  padding: 4px;
  border: 3px solid #000000;
  background-color: #FFFFFF;
}

With the CSS in place, add the following pascal classes to your mainform code, just below the “type” keyword:

TTestChild = class(TW3CustomControl)
end;

TTestOwner = class(TW3CustomControl)
end;

Now, prior to writing this article I make a couple of helper functions. If you are using Alpha 1 (which most of you are), add the following class and code to your form1 unit:

TW3Theme = class
public
  class function  AdjustRectToLayoutFactors(const ThisControl: TW3MovableControl; const Rect: TRect): TRect;
  class function  GetPaddedClientRect(const ThisControl: TW3MovableControl): TRect;
end;

class function TW3Theme.GetPaddedClientRect(const ThisControl: TW3MovableControl): TRect;
begin
  if ThisControl <> nil then
  begin
    result := TRect.Create(0, 0, ThisControl.ClientWidth, ThisControl.ClientHeight);
    result.Left += ThisControl.Border.Left.Padding;
    result.Top += ThisControl.Border.Top.Padding;
    result.Right -= ThisControl.Border.Right.Padding;
    result.Bottom -= ThisControl.Border.Bottom.Padding;
  end else
  result := TRect.NullRect;
end;

class function TW3Theme.AdjustRectToLayoutFactors(const ThisControl: TW3MovableControl; const Rect: TRect): TRect;
begin
  if ThisControl <> nil then
  begin
    (*  Rule #1, "margins should only be added when dealing with child elements"
        Since we are usins "border-box" as our size model, padding and border is
        already included in the clientwidth / clientheight values we get from the
        system.

        More importantly, margin only affect left and top edge of a rectangle because
        those are the only factors that affect MoveTo() type functionality in
        the browser itself.

        So when the browser moves an element to position 10px, 10px, it automatically
        adds the margin. If you have a margin of 10 pixels - the result will be
        (visually) that the control ends up at 20px, 20px instead. *)

    // Start with a carbon copy of the rectangle we were given
    result := Rect;

    (*  Note: The browser can only know about the left and top edge when
        placing elements. It cannot see into the future to know the exact
        height of an element, or if the content will suddenly grow. So we
        have to calculate the right and bottom based on our knowledge
        from the Rect parameter *)
    result.right  -= (ThisControl.border.left.margin + ThisControl.border.right.margin);
    result.bottom -= (ThisControl.border.top.margin + ThisControl.border.bottom.margin);

    (*  Rule #2: Padding should only be applied from a control's padding when
        calculating a position for that child. This is recursive, so a parent
        will apply this to their children, which each child will force it's
        padding on any children it may house. *)

    if ThisControl.parent <> nil then
    begin
      var Owner := TW3MovableControl(ThisControl.Parent);
      result.left += Owner.border.left.padding;
      result.top += owner.border.top.padding;
      result.right -= owner.border.right.padding;
      result.bottom -= owner.border.bottom.padding;
    end;
  end;
end;

With both styling and the pascal classes out-of-the-way, lets add some code to get the magic working.

So copy & paste this into your W3Form1.InitializeForm() procedure:

  var LRect:  TRect;
  var Box:    TTestOwner;
  var Child:  TTestChild;

  // Create parent container
  Box := TTestOwner.Create(W3Panel1);
  LRect := TRect.Create(0, 0, W3Panel1.ClientWidth, 300);
  LRect := TW3Theme.AdjustRectToLayoutFactors(Box, LRect);
  Box.SetBounds(LRect);

  // create child element for our test-owner
  Child := TTestChild.Create(Box);
  LRect := TRect.Create(0, 0, box.ClientWidth, box.ClientHeight);
  LRect := TW3Theme.AdjustRectToLayoutFactors(Child, LRect);
  Child.SetBounds(LRect);

Note: The TW3Theme class is a part of Alpha 2 which should hit the download section next week. But you should now have everything you need to get this working, no matter what version of Smart Mobile Studio you are using.

Putting it all together

OK, lets run our application and have a look at the results. What we should see is a panel on a form – inside that should be a box that is 20 pixels from the edges (since the CSS defines 20 pixel margins). The box also has 4 pixel padding defined, so the total offset from the edges should be 24 pixels.

The child control inside the box likewise has margin and padding. It operates with 10 pixels of margin and 4 pixels padding. It also sports a 3 pixel border. So let’s see what we have so far:

marginals

As you can see it’s not that hard to deal with; just a bit of a brain teaser. Those that write custom controls for Delphi are used to dealing with stuff like this all the time. The difference is that native languages are less cryptic about things, and they also make width / height return the full size of the control. Regardless of what the content may be.

You might have noticed that Delphi has a new “Align with margin” property? Not sure when it came into the system but somewhere around Delphi XE I believe (?). There you define the size of the margin – and Delphi does the rest. You don’t have to think about the size of the margin, and it only comes into play when the Align property is activated.

Final notes

We are doing some brainstorming on how to best deal with these things right now. Personally I think the code I have shown so far, especially the helper code, goes a long way to make this easy to work with.

Some have voiced that ClientRect should always start at zero, but why is that? Where does it say that Clientrect should always be “0,0, width-1, height-1” ? That is not the voice of reason, that is the sound of old habits! The whole point of having a ClientRect, be it in Delphi, Lazarus, C++ or C# is because this can change. It would be equally futile to demand that ClipRect should always be the same as client-rect. That is to utterly miss the whole point of sequential rendering and fast graphics.

So the lesson is: If you play by the rules and never use hard-coded values, then your code wont be affected. And if you want to adjust so your code is 100% theme compatible (and again, this only is valuable for component writers) then calling a simple function to get the rectangle adjusted for margin etc. is not exactly rocket science. It’s a one-liner.

Well, hope it helps!