pglet-python icon indicating copy to clipboard operation
pglet-python copied to clipboard

Reactive columns challenge

Open mikaelho opened this issue 3 years ago • 7 comments

Having a variable number of columns, typically 2-3, with some minimum widths, the columns should get stacked vertically when they can no longer fit side by side, and still take all available horizontal space.

For example, if I have 3 equal-sized columns, they would all take about 33% of the horizontal space when all on the same row. If the display is too narrow to fit them all and one column wraps to the next row, the two columns on the first row would both have 50% of the space, and the last column now on its own row 100% of the horizontal space.

This is reasonably typical reactive layout, but I have yet to find the way to make it happen with pglet-python. There is a promising value for Stack.horizontal_align ”stretch”, but in my experiments it has not had a visible effect.

Much appreciated if you could share a way to make this happen. (Or a feature suggestion if not possible yet.)

mikaelho avatar Dec 19 '21 13:12 mikaelho

That's an interesting topic to discuss. Stack control is an implementation or "flexbox" layout which is sufficient in the most cases to implement page layout of any complexity. Stack reminds Panel control from ASP.NET or Delphi if you will. There is also "[grid]"(https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/Relationship_of_Grid_Layout) layout implemented by many CSS frameworks.

Now, "responsive layout" is usually implemented as "breakpoints" with "media queries". My initial idea was to implement grid layoutrow and column controls with media-specific props, but on a second thought I realized it would be too HTMLy and hard-to-understand for developers not familiar with CSS frameworks. You know, it could be a real mess with all this media-specific classes: https://getbootstrap.com/docs/5.1/layout/css-grid/#responsive

What we could try doing instead is to add resize event to Pglet Page control (and width, height properties), so you can explicitly resize any controls on the page in the handler of this event. Yes, that would be some lag between resizing browser window and layout changes, but I feel it could be acceptable in the most cases.

Some pseudo code:

import pglet
from pglet import Stack

def main(page):

  left_column = Stack(width='30%', height=40, bgcolor='CyanBlue10')
  center_column = Stack(width='30%', height=40, bgcolor='CyanBlue10')
  right_column = Stack(width='30%', height=40, bgcolor='CyanBlue10')

  page.add(
      Stack(horizontal=True, wrap=True, gap=5, width='100%', bgcolor='#ddddee', controls=[
          left_column,
          center_column,
          right_column
      ])
  )

  def page_resize(e):
      if page.width < 576:
          # small device
          left_column.width = center_column.width = right_column.width = '100%'
      else:
          # device with a large screen
          left_column.width = center_column.width = right_column.width = '30%'
      page.update()

  page.on_resize = page_resize

pglet.app("page-resize", target=main, web=False)

or resize event could be even implemented for Stack control as well. What do you think?

FeodorFitsner avatar Dec 19 '21 22:12 FeodorFitsner

Just to check an idea I've quickly implemented page.resize (program above was updated with a real one). Take a look how it works:

https://user-images.githubusercontent.com/5041459/146712281-0e20c22d-ebd6-4942-a1ac-4625d0691a2e.mov

FeodorFitsner avatar Dec 20 '21 04:12 FeodorFitsner

Thanks, looks excellent!

As you say, the performance is not comparable to browser-native flexbox resizing, but this approach is explicit, centralised and powerful -- i.e. this way it much clearer how to e.g. make a left-hand menu that is always visible on large screens, but opens up from a button in small ones.

+1 for the Stack-level resize. I think we would not want to have all the resize-reaction code at the top level, as it would make it difficult for reusable components to take advantage of this feature.

As a convenience, I could think of a resize_sizes or similar attribute (to any control that has controls?) which would be e.g. a dict like:

{
    0: "100%", "100%", "100%",
    576: "33%", "33%", "33%",
}

... to try to avoid the basic repetition of always writing a very similar resize handler.

Or, even better, a method that gets the threshold minimum full width and (optional) share per column, and have the calculations done for you, taking into account gaps, margins and paddings. Or are we just re-inventing the flexbox here?

mikaelho avatar Dec 20 '21 05:12 mikaelho

Getting back to this after a little break.

I experimented with a flexible reusable column control (stack) here. It generalises from your example to take any number of controls, works both horizontally and vertically, and has an option to stretch the last row/column or to keep items there the same size as all the others.

It works fine, but only as long as it is essentially the root control, i.e. the width/height of the window.

It would be much more flexible if it could react to changes to the size of the control, not the size of the window, or at least if it would have access to the pixel size of the control (now I only get e.g. "100%").

Any ideas how to make those happen?

mikaelho avatar Mar 20 '22 17:03 mikaelho

I must say it looks nice :) Well, the main drawback of reacting on page resize event though it's layout readraw latency.

Why would you need the size of container? For example, most of the popular CSS frameworks introduce "breakpoints" based on screen size. I'm not sure if HTML layout model allows knowing container size before drawing children, but looks like Flutter has that ability. Although sending over the wire selected container sizes while drawing them could be an additional overhead.

FeodorFitsner avatar Apr 04 '22 16:04 FeodorFitsner

Thinking further...let's say I have a "big" 1920x1080 display and made a page with 2 columns - 1st one is 200px wide and 2nd filling the rest (1720px). You mean if I put your component to the first column it would think it's being drawn on a "small" screen and all its content would be layed out in a fewer columns? See, HTML rendering model gives all real dimensions only after entire document is loaded/rendered. Imagine, you can easily set width of your control to 2,000px and it would happily expand behind the screen. In Flutter though it's the opposite - all constraints go down, from parent to children, so your scenario is doable in Flutter, i.e. the component knows parent constraints while being built. After playing a bit with Flutter layout system it feels more modern and well-thought than HTML. That another reason we should switch to it asap :)

I'm sure we could bring to a Flutter-based UI a declarative approach similar to Bootstrap layout rather than imperative approach with a code reacting on screen size changes.

FeodorFitsner avatar Apr 04 '22 18:04 FeodorFitsner

So, yeah, your idea with resize_sizes in this comment is what I'm currently having in my head too.

FeodorFitsner avatar Apr 04 '22 18:04 FeodorFitsner