-
Notifications
You must be signed in to change notification settings - Fork 29.8k
Description
I raised this a while ago, but then gave up and closed the issue. It is a fundamental issue though so I'm reopening it.
Consider the following snippet:
runApp(
Align(
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.only(left: 20, top: 20),
child: Container(
decoration: BoxDecoration(
color: const Color.fromRGBO(255, 166, 0, 0.428),
border: Border.all(color: Colors.yellow, width: 1)),
width: 64,
height: 45,
child: Center(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.yellow, width: 1)),
width: 42,
height: 22,
),
),
),
),
),
);When you run this on a device with 100% pixel scaling, you get the following result:
A commonly used 4k 27" monitor on windows will run with 150% pixel scaling factor. Running any Flutter application on such setup, you can pretty much guarantee that every single widget border will be blurry. It is near impossible to deliver a sharp looking Flutter application on Windows that works correctly with fractional scale factors (and as mentioned above sometimes even with a basic 100% scaling).
This is a problem that has been solved pretty much by every modern toolkit (WinUI, Compose, Browser) except for Flutter, with keeps stubbornly pretending that it is rendering into an infinite resolution surfaces, which is just not the case.
There are many places where this causes issues i.e. #14288, #90926, #25531, #31707 or #143420, last of which has been solved with a really ugly hack.
Another big issue stemming from this is that all layout in Flutter is performed in logical pixel space on floating point numbers. Floating point math is not associative, which causes many subtle and difficult to find issues, such as this:
Proposed solution
The solution for this issue is rather straightforward, unfortunately it's also a major breaking change which makes applying it difficult.
- Remove the device pixel ratio scaling transform from the app. Using transformation for this is simply too blunt and causes the issues above. This transform creates an illusion that we don't need to deal with the presence of physical pixels, but this quickly falls apart when running of anything with less than 200% device pixel ratio.
- All user specified unit (i.e. border size, padding, dimensions, font size) would be specified in device independent coordinates. This could even be an extension type to make things explicit, though I'm not sure what would happen with existing geometric primitives like
OffsetorRect. - All layout would be performed in physical pixel coordinates and integers.
- All painting would be performed in physical pixel coordinates.
The conversion from logical to physical pixel space is a simple scale + round(). i.e.
For example imagine that you have 1px border around widget and 1.5 device pixel ratio. In which case
physicalPixels = (1 * 1.5).round() = 2
This would offset the layout by two physical pixels, and would also paint as two physical pixels.
Constraints and geometry would probably need to be converted to integers and in physical pixel space. There is an issue where some of the Widget API is using constraints (i.e. LayoutBuilder, ConstrainedBox), we would need to have a convenient way to convert back and fort from physical to logical pixel space.
Changes to layout
All layout would be performed in physical pixel space on integral coordinates. This would ensure that under normal circumstances (no arbitrary transform in the app), all widgets would be laid out on exact physical pixel boundaries. This would require some changes in the layout algorithms, for example consider flex with 3 expanded widgets constrained to 100 physical pixels. The flex can only produce integral coordinates and must fill all the space, so the final dimensions of the widgets would be 33, 33 and 34 pixels.
Arbitrary scale transforms
Most Flutter applications likely don't use arbitrary scale transforms, apart from transient things like a zoom effect on hover. I'd wager that in majority of Flutter application there is exactly one persistent scale transform in the layer tree - the device pixel ratio scale. With the changes proposed here this would be removed, replaced by manual scale + roundToInteger in appropriate places in framework.
Of course nothing prevents user from adding other transforms that will not respect physical pixel boundaries, but that case aligning to physical pixels is likely not a concern so it is orthogonal to the proposal here.
Engine changes
This would not require any engine changes at all. This needs to be all handled at framework level, since it impacts layout. If done correctly, final transform in the layout tree will all be pixel snapped by the time they reach the engine.
Migration
This is the tricky part. If anyone has an idea of how we could gradually migrate Render Objects, Constraints, Geometries and painting to physical space and integral coordinates (for layout) I'd love to hear about it.