Humble Chronicles: The Layout

This is a second post documenting the process of developing Humble UI, a Clojure UI framework. In this post, we discuss Humble UI approach to layout.

None of the decisions are complete or final and might change at any time. The main purpose of these posts is to share ideas and get a better understanding of what can work and what can’t. Feedback is welcome!

UI Scale

The way I like to think about it is this:

Logical sizes stay the same between screens, physical sizes vary depending on pixel density and OS settings.

19 logical pixels are still 19 logical pixels, retina or not

Logical pixels are converted to physical ones by multiplying them to UI scale. Old-fashioned 1080p screens usually use UI scale of 1.0. Retina screens use UI scale of 2.0. For Windows and Linux, UI scale could be any number: 1, 1.25, 1.5, 1.75, 2, 2.5 are all reasonable UI scales.

Humble UI approach is simple:

So there could be no 20.5 physical px button in Humble UI, but 20.5 logical px—why not? As long as physical result fits the physical pixel grid, we are happy:

UI scale is not a hard-coded number, does not have to match OS setting and everything in Humble UI is scale-aware and ready to render at any scale.

By default, it is used to match OS settings, but can also be used to easily zoom your UI per window:

Layout model

Internally layout is implemented through IComponent protocol:

(defprotocol IComponent
  (-measure [_ ctx size])
  (-draw    [_ ctx size canvas]))

-measure returns component’s intrinsic size, given the available space of size.

-draw must draw a component filling given size.

Draw calls are top-down: starting from the outermost container, we divide space and ask internal components to fill it.

Measures are bottom-up: if you want to measure a container, it asks its children to measure themselves, then combines the results.

If you put a button directly into a window, it will fill the whole window:

Under the hood, a window will just ask the button to -draw with a known window size:

(-draw button ctx {:width 600 :height 400} canvas)

Alternatively, if you wrap the button with align to center it, the situation will change:

So far this model proved sufficient enough, we’ll see how much it can handle as Humble UI evolves.

Stretch by default

Button stretching to the full window might sound counter-intuitive at first, but if you think about it, it’s the only logical thing to do: button inside a container without anything else must fill the whole container.

Now, if you want different behavior, you must specify it further. Do you want it aligned to the top left corner? Left centered? Centered stretched? None of these sounds like an obvious default, so you must add it explicitly.

Additive model

Humble UI favors small single-purpose components over settings or modifiers because they tend to compose better.

E.g. there’s no setting on a button to set its width to 300px, but there’s width component that can set width to any child it contains. Same for vertical/horizontal alignment, padding, etc.

This:

(ui/halign 0.5
  (ui/valign 0.5
    (ui/width 300
      (ui/padding 10 10
        (ui/button "Click me")))))

Will get you this:

Basically you add more and more stuff until you get what you want. Don’t like something, or want to do something differently? Pull it apart, replace, add, all without modifying components themselves.

Looks a little verbose, but hopefully this approach will be more “simple than easy” and faster to understand than, say, CSS.

The tricky part was to figure out a model that actually combines well. At various stages, I’ve seen component stretching or not stretching where expected, halign not being able to be nested inside valign and vice versa, halign not working inside column/valign inside row and many others.

But all well that ends well, and I am happy with where we are at right now:

Gaps and margins

One thing I feel very strongly about is margins in CSS. Consider following UI:

If you try to describe it with words, you would say something like “profile picture, profile link and logout link with 10 px between components”.

We think about 10 px spacing as its own thing, part of the layout, not part of the components. We don’t say “profile picture with 10 px right margin” because that’s not how we think.

In CSS, however, the way to add spacing between components is to modify components themselves, adding the right margin, like this:

.userpic { margin-right: 10px; }

There are three main problems with this:

  1. Components with margins are hard to reuse. That’s because margins make sense only in a certain context. They belong to the container, not to the component.
  2. First/last components need special treatment because you don’t want 10 extra pixels there.
  3. How do you decide if one should use the left or right margin?

Space between components doesn’t naturally belong to one or the other. It’s literally between them.

So naturally Humble UI does not have margins. What do we have instead? Gaps!

(ui/row
  userpic
  (ui/gap 10 0)
  proflie
  (ui/gap 10 0)
  logout)

Gaps are great because they are simple things that you add to your layout instead of modifying existing components. Logically they live on the same level as other container children, which is where they belong.

They are also easy to work with and have a simple conceptual model: they are just blocks, like any other block.

Universal align

Align idea is stolen from Flutter, with the only difference being that I split horizontal and vertical aligns into two separate components.

To align something inside a container, you specify two numbers: percentage of the child’s width and percentage of the container’s width.

(<container>
  (ui/halign 0.6 0.2
    <child>))

will get you:

What’s cool about this model is that it gives you all the usual alignment modes:

(ui/halign 0.0 0.0) => left
(ui/halign 0.5 0.5) => center
(ui/halign 1.0 1.0) => right

and then some:

Text bounding box

If you read my deep dive into font construction, you know that I don’t like how text is aligned right now on the web. Humble UI feels like a good moment to revisit it and improve on the status quo.

In short, text boundaries in Humble UI are defined by baseline and cap-height, not by ascender/descender/em square or any other arbitrary unit.

What are the upsides?

Text inside buttons is easy to center:

Baseline alignment is the same as center alignment:

Text aligned to picture looks good by default:

Line height is easier to control (remember gaps?):

But wait, what about ascenders/descenders? They will render outside the boundary box? Yes they will:

I don’t consider that a problem because most of the time you add extra padding around text blocks anyway.

Dynamic sizes

Remember calc from CSS? Well, you can do a similar thing in Humble UI, but using plain Clojure code:

(ui/width
  #(-> (:width %)
     (- (* 3 padding))
     (/ 2)
     (+ padding))
  (button "C" color-clear))

Variables, functions, macros—all at your fingertips.

Works everywhere a dimension is needed: e.g. in padding component.

What’s next?

Notable missing parts are:

Hope to add them soon. Meanwhile, you can play with the code here.

Am I missing something else? Do you have an opinion? Make sure to let me know!

Hi!

I’m Nikita. Here I write about programming and UI design Subscribe

I also create open-source stuff: Fira Code, AnyBar, DataScript and Rum. If you like what I do and want to get early access to my articles (along with other benefits), you should support me on Patreon.