Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-values] PanResp (Panel Responsive) support: sizing expression mapping #3874

Open
sdwlig opened this issue Apr 25, 2019 · 15 comments
Open

Comments

@sdwlig
Copy link

sdwlig commented Apr 25, 2019

https://drafts.csswg.org/css-shadow-parts/
This could be added to this specification, or a new specification might be more appropriate. However, I'll suggest it here as it is part of the problem this spec is solving and it would be better to have this sooner. The change is small, easy to implement, shouldn't have any effect when not used, and no security or other negative implications when used.

I created and have been using a new technique for a better kind of responsiveness that I call PanResp - Panel Responsive (where panel means any element, div, etc.). Without going into too much detail here, the point of this method is to have fine grained control over sizing of information on a per-panel basis, allowing for breakpoints after which all content is scaled down smoothly, in partly non-linear fashion. This technique also allows independent per-panel zoom. I'm in the process of publishing details, examples, and an npm package. While it would be great if the technique was native to browsers, it isn't too difficult to add to an application, except for being able to completely update the style of elements. Each size-specifying CSS variable needs to use a small formula that uses CSS variables.

With custom elements, this means that every size-specifying CSS declaration has to be updated. Normally, the results of this update look the same as before the update until a responsive scaling is activated. Because of shadowRoot, if everything is not exposed, a parallel modified version of every library is needed.

Modified versions of libraries could be avoided, and not affect stylability choices otherwise or require parallel versions of libraries, if there were a way to transform all size-specifying CSS expressions with the same scoping as a CSS variable. I.e., flowing downward through the DOM with different element trees using different expressions and/or variables.

What I propose here is to have a way to indicate a rule, perhaps as a CSS variable or something that works like a CSS variable, for each CSS sizing type: px, em, rem, pt, etc. The rule would take the value or expression result and place it in an expression that could map to another type with a factor. For example:

  • --css-size-type-mapping-px: calc(PXvw*var(--font-scale, var(--lfont-scale)))
  • --css-size-type-mapping-em: calc((16EM)vwvar(--font-scale, var(--lfont-scale)))

where PX is the original expression, like 16px, and the EM expression might be 1.5em. Internally, this mapping can happen once each time a CSS expression is changed, then handled normally on each evaluation.

Actually being able to run Javascript to perform computation and mapping would be even better, similar to the rendering and other callbacks that are coming, but this simple expression mapping solves the shadowRoot problem well enough.

@tabatkins
Copy link
Member

What I think you're proposing here is a way to change the "size" of certain units, right?

An easy way to do this today is to use a variable as a "base size" rather than using units directly; that is, write width: calc(var(--px) * 5) instead of width: 5px. It's definitely verbose, but it works everywhere. ^_^

At some point in the nearish future we'll have custom functions, so you could instead write this as width: --px(5);; this'll be backed by a JS function, so you can map it to a length however you want. (This isn't specced yet, but they're the next thing on my list for CSS Houdini specs.)

All in all, I think leaning on this sort of functionality makes more sense here. Adding a new implicit-conversion functionality that makes a length like "16px" not be 16px would end up being more confusing, I think. For example, what happens if someone writes --foo: 20px; on an element with this sort of mapping? If it's unregistered, the length won't be interpreted as such; it'll inherit down as the value "20px" and be interpreted with whatever the mapping is on the descendant that uses var(--foo); if it's registered, it'll be converted early and inherit as the altered size. We already have this sort of "early or late" problem with some functions in variables; extending it to all lengths would be pretty confusing, I think.

@tabatkins
Copy link
Member

And then, beyond the theoretical "let's ignore implementation concerns" design issues, the fact is that this is another design like the em versus 'font-size' design. In implementations, this means they have to specially handle 'font-size' before most other properties, so they can compute em lengths properly everywhere else. This is useful and fine, but it's complex, and browsers try to limit the number of additional properties they have to process similarly. Any of these properties would also be in this category, so browsers would be loathe to add them without a very good use-case backing them.

@sdwlig
Copy link
Author

sdwlig commented Apr 26, 2019

What I need specifically, and what I think may become popular once I publish this technique, is the ability to map all of the sizing CSS units in existing HTML/CSS/Javascript code, libraries basically, without modifying them. I already have techniques for applications to use, and I already have to modify each library for use within this scheme. What I need is a way to do the same thing without actually modifying as much code, or any code.

In other words, I have a valuable technique for web app development that requires that all CSS sizing functions be mapped as above, but doesn't require any other accommodation. The resulting transformation is essentially a no-op until the system needs to activate scaling. The CSS is intended to result in the same or roughly the same layout, but be able to scale according to certain rules based on a parent. This is a bit like the em nested sizing rules, although much more useful.

@sdwlig
Copy link
Author

sdwlig commented Apr 26, 2019

I do have a very good use case, although I haven't published or established that quite yet; I will be focused on that in a few days. This is a new, very useful technique for responsively scaling content that is A) often better than existing methods for page wide content and B) allows responsive scaling of content in panels that is essentially impossible with other methods.

For the latter, consider how you would accomplish:
Two or more side-by-side panels, i.e.. divs with hierarchies of content, divided by splitters, draggable dividers that change the width of the panels. Regardless of content, you want the size of the content in each panel to remain static down to a certain width wrapping as needed, below which everything should scale linearly or non-linearly with maximum readability, no longer wrapping etc. In some cases, you may want everything to be able to scale down to nothing. The Javascript involved at each point should be very minimal, not walking the DOM with CSS adjustments or anything complicated.

I can already do that if I comprehensively modify the CSS used in the contents of panels. That will mean that only libraries and application code that have been converted can use this technique. With my proposed change, any or just about any existing HTML+CSS content would work with no modifications. I'd rather not have to create shadow versions of every desired library. This appears to be the simplest change that would accomplish that. I think it might be possible to create a polyfill that does this, walking the DOM even inside shadowRoots, updating CSS, but that will be expensive and not a long-term solution.

@sdwlig
Copy link
Author

sdwlig commented Apr 26, 2019

This would require a change to how expressions are handled, but it would be very confined: At the point that the unit is required in a subexpression, rewrite the expression at that point by replacing unit with the given formula with replacing the unit variable. Or, looked at a different way which I think would be equivalent here, this could be done as the value of each unit in any expression. So this becomes more like 'get px() { return vw*var(--font-scale, var(--lfont-scale)) }' rather than 'get px() { return this.px; }'. Then the CSS expression parsing & evaluating itself shouldn't need to change, just the sizing unit retrieval.

@sdwlig
Copy link
Author

sdwlig commented Apr 26, 2019

Other than replacing what px, em, rem, pt, etc. mean, nothing else changes or needs special handling. I'm already doing this manually with existing browser engines, so I know it doesn't require any other special handling. When a change is needed, CSS variables change (one for each type of scaling: linear, optimally readable font related non-linear, and linear below break point fixed above) and everything maps accordingly. Internally, the CSS engine is mapping all of those to some current pixel unit at some point anyway. This is just a direct, static during layout computation replacement of the current type->pixel value retrieval.

Different type expressions could use different CSS variables, which allows em/rem/pt to use font scaling while px would be linear or linear/fixed depending on choice in a parent. The vw & vh types might need special handling. Offhand, the obvious solution is to be able to indicate the basis for vw & vh rather than the browser window viewport. So: 'get vw() { return --vw-element.width / 100; }' rather than 'get vw() { return viewport.width / 100; }'. And that should not be applied to the other sizing type expressions.

@sdwlig
Copy link
Author

sdwlig commented Apr 26, 2019

I didn't address the early / late issue: I don't see why any early computation is needed. I'm already using CSS as is. The substitution is only needed at the very last step of combining actual current pixel size of a unit in an expression. The problem I'm trying to solve here is avoiding comprehensively modifying CSS in other people's libraries just to use them in this environment. I've already done that a lot, but it's not a great solution.

@tabatkins
Copy link
Member

For the early/late issue, here's an example:

.parent {
  px-mapping: calc(PX * 2);
  --foo: 20px;
}
.child {
  px-mapping: initial;
  width: var(--foo);
}

In this, what's the size of the child? 20px or 40px? I suspect the answer depends on whether --foo is registered or not: if registered, it'll be processed as a length, and get the mapping, becoming "40px"; if not, it'll inherit as an unaltered "20px" token, and then the child gets that and becomes 20px wide.

@sdwlig
Copy link
Author

sdwlig commented Apr 26, 2019

Does the browser currently do early or late reduction of a sizing unit to actual pixels? em is problematic, but does it bind early or late? If I have --fs: 3em, is that evaluated to actual pixels in the parent or in the expression where it is used relative to the local font-size? I expect the latter, which is perfect for this.

The expected use of this is from a certain high-level point in the DOM hierarchy down so that these kinds of variables would typically be defined within the same realm, all having the same unit * scaling values.

Your example does not illustrate what I am proposing. 20px would stay 20px all the way until it was actually used to size something. So, neither early nor late binding, but very late binding. There is no point where 20px becomes 40px in any CSS expression. To mix pseudocode for logical internal functions and CSS a bit:

pxInPixels() { if (--px-map) return cachedEval(--px-map); else return window.pxPixels; }
emInPixels() { if (--em-map) return cachedEval(--em-map); else return window.pxPixels * emBase; }
.parent {
  --px-map-bad-example: calc(PX * 2);
  --lvw-scale: .0833; // 1200 px nominal 100% width.  This is the starting / fallback value, set in each webcomponent / environment.
  --vw-scale: .0833; // Set dynamically as needed, once at each top panel.
  --px-map: verylatecalc(PX*vw*var(--vw-scale, var(--lvw-scale)) // only eval at last step
  --foo: 20px;
  --alsoFoo: 20;
}
.child {
  px-map: initial; # default, not necessary.  Overriding prevents scaling for this component.
  width: var(--foo); // still 20px
  min-width: var(--alsoFoo)px;
}

Where cachedEval simply means memoizing until the dependent CSS variables change. And where px, em, pt, and especially vw have their unmapped meanings.

@sdwlig
Copy link
Author

sdwlig commented Apr 26, 2019

The clarify a bit more how I use this:
Every browser viewport is 100vw wide by definition. A browser window viewport that is 1200 pixels wide, is also vw/1200 * 1200 units wide, or each pixel is .0833vw wide. So rather than use pixel width breakpoints with alternative CSS for font-size, widths, etc., an app may want to treat 1200 pixels as 100% and, at some lower width start scaling down smoothly rather than breaking (clipping to the right, wrapping in ugly ways). In some cases, parts of a layout should stay exactly proportional while in others a layout should stay completely fixed, even with browser zoom changing.

Using vw as a basis for CSS sizing expressions has been done, although it is a bit of a toy on its own for a couple reasons. The worst of these is that there is no per-element vw / vh, but even if there were, it wouldn't be too helpful. A designated parent for vw / vh gets closer to being useful.

It turns out that controlling scale for several reasons can all be done by a CSS variable along with vw. Potentially vh also, but experience has shown that vw plus factoring in height to algorithms is best overall.

@tabatkins
Copy link
Member

Does the browser currently do early or late reduction of a sizing unit to actual pixels?

A custom property using an em value would have exactly the possibly-confusing behavior I outline: if it's a registered property, it turns into px based on the parent's 'font-size' value before inheriting; if not, it stays as an em value and becomes px based on the child's 'font-size' instead.

Spreading this behavior to every length unit isn't great.

@sdwlig
Copy link
Author

sdwlig commented Apr 26, 2019

I am explicitly suggesting a mechanism that does not have have that problem, as I detailed above.

@tabatkins
Copy link
Member

There is no way to avoid that problem; non-registered custom properties are never parsed into meaningful values until they're actually used in a var(), so the browser simple doesn't know they contain a length at the time they're declared.


That all said! My second post outlining the implementation difficulties was on the money here; these mapping properties would be additional "must find computed value before everything else" properties, which are a bit expensive/complicated to add.

At the moment, your use-case seems interesting, but theoretical, and I don't think I can justify trying to push a large new change like this from it.

However, please pursue your technique! If it ends up popular, it'll (a) tell us that we could help a lot of people by making it simpler to use, and (b) give us real-world data on exactly what works well or less well for users, so we can design the feature to be the best possible.

@tabatkins tabatkins changed the title [css-shadow-parts] PanResp (Panel Responsive) support: sizing expression mapping [css-values] PanResp (Panel Responsive) support: sizing expression mapping Apr 26, 2019
@sdwlig
Copy link
Author

sdwlig commented Apr 26, 2019

Either I don't understand something or I am not communicating well: Your first paragraph seems to indicate that leaving these CSS variables non-registered would prevent the very problem that you are pointing out. Or at least non-registered in that complete, compute-now sense.

The browser should not care that they contain a length when they are declared. The CSS variable can simply be a string all the way until the last step each time it is used. The value only needs to be computed when a CSS property needs an actual value in layout, such as font-size: 2em. In the most straightforward implementation, the 2em wouldn't be replaced. If --em-map was set, the internal browser function that returns the pixel (or whatever is used) value for em would use the given mapping expression to return what em means at that point. Nothing else changes, no other accommodation is needed.

I'll be publishing my PanResp method and library soon. And I'm going to look into creating a polyfill for this; should be an interesting challenge.

@tabatkins tabatkins added this to Needs triage in Blink Issue Triage via automation Apr 26, 2019
@tabatkins tabatkins moved this from Needs triage to No Action Needed in Blink Issue Triage Apr 26, 2019
@sdwlig
Copy link
Author

sdwlig commented May 3, 2019

I am explicitly suggesting a mechanism that does not have have that problem, as I detailed above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Blink Issue Triage
  
No Action Needed
Development

No branches or pull requests

2 participants