slint-ui / slint

Slint is a declarative GUI toolkit to build native user interfaces for Rust, C++, or JavaScript apps.
https://slint.dev
Other
17.72k stars 615 forks source link

Fractional border rendering #5747

Open Enyium opened 4 months ago

Enyium commented 4 months ago

This issue serves the purpose of discussing (and concluding) on whether to support synthetic borders / lines or not.

Original description:

The only method of Window that allows you to register a callback is on_close_requested(). I think adding the method on_scale_factor_changed() would make sense. The callback would need to be called when the user is dragging the window onto another monitor with a different scale factor, or if the system settings change. Maybe you'd also or instead want to provide a solution for .slint code.

Usefulness: When drawing a diagram with a grid implemented via Rectangles, you may want to ensure a minimum grid border width of 1 phx as well as round the width to whole phx, so that (I hope) no effect of the kind "2 phx border, 1 phx border, 2 phx border, 1 phx border" comes to be. With the pixel snapping approach Slint seems to use for everything but Path (as opposed to pixel-fractional rendering like SVG in the browser), these larger–smaller effects should be reserved for larger objects (like the areas between grid lines) where it's not that noticeable if they differ in size by 1 phx.

(I'm currently not sure if a clever calculation in .slint code involving a user-defined length, 1px and 1phx would already allow you to ensure the above border constraints, since Slint updates the scale factor when dragging the window to another monitor as the rendering shows.)

If Slint could render everything pixel-fraction-accurate like SVG in the browser and like it's already doing with Path, diagrams may look even better, though. Is that realistic for future Slint?

tronical commented 3 months ago

I'm not sure about adding such a specific callback to Window, TBH. We could indeed consider adding a hook for all platform window events dispatched from the backend, for application to "see" them. But for this specifically I feel #112 may be better suited.

Regarding pixel fraction rendering: We do not currently round to the pixel grid. That is a semi-recurring criticism, in fact, to the extend that we've been asked multiple times to implement it to reduce blurriness.

Enyium commented 3 months ago

Regarding pixel fraction rendering: We do not currently round to the pixel grid. That is a semi-recurring criticism, in fact, to the extend that we've been asked multiple times to implement it to reduce blurriness.

I don't understand why you would say that. My test seems to speak another language. Screen capture on Windows 10 with a scale factor of 1.25 and Skia (FemtoVG is the same):

https://github.com/user-attachments/assets/5c695f9b-3f95-4dc7-a938-296c8dfc7c0b

https://github.com/user-attachments/assets/8df2905b-bbc4-49f4-9466-dd2f43ce2df9

I don't know about low-DPI screens, but the right squares look a lot better and more suitable for diagrams to me. The diagram I'd like to implement would have independent x- and y-zoom and configurable line thickness.

It's just that Path currently seems a bit buggy or hard to tame, and it's the only element I discovered which renders pixel-fraction-accurate. (I'm not talking about anti-aliasing of rounded Rectangle corners.) This may also mean that Path lines wouldn't harmonize well with other pixel-snapped elements that you'd like to render between grid lines.

Enyium commented 3 months ago

The rendering smoothness can be improved a bit to look more like on the right side by specifiying border-radius: 0; on Rectangle. But only for FemtoVG and not for Skia. So, you definitely have different rendering behavior, which should actually be more predictable and definable.

ogoffart commented 2 months ago

I'm actually confused what this issue is about. I don't think we need a callback on scale factor change. Isn't it enough to just use 1phx or callback on changes. What would you do in such callback that cannot be done with bindings with math on phx and px?

Enyium commented 2 months ago

So, of course, my Slint knowledge is steadily growing. Not all of these recipes are documented or readily obvious.

tronical commented 2 months ago

I've taken the liberty of editing the issue text and title. I think we need to decide if we want to support synthetic lines / borders (that are guaranteed to be 1 physical pixel wide) or not.

My feeling is that we should support that, but I'm also not entirely sure how to best phrase it API wise.

Should we say that border-width: 1px will not be a synthetic border and that we could somehow support writing border-width: 1phx to achieve that?

cc @ogoffart @NigelBreslaw

Enyium commented 2 months ago

In WPF, there's the setting SnapsToDevicePixels. Maybe, users could enable a Slint analog to this in an MCU low-DPI context to prevent blurriness. It could also be a setting on a per-element basis (with effect-wise inheritance), like the WPF docs describe it.

Note that, e.g., in a diagram with a grid that is regularly scrolled, changes its per-axis zoom and is otherwise transformed, the elements between grid lines including text should also have the option of being rendered pixel-fraction-accurate.

In Firefox, this currently doesn't work (no pixel-fraction-accurate translations), but in Brave (Chromium), you can visit https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text, click on the "Play" button of the code block, and replace the four <text> elements with the following animating ones to see that the text is rendered pixel-fraction-accurate:

  <text x="20" y="35" class="small">
    My
    <animateTransform attributeName="transform" type="translate" from="0 0" to="0.5 0" dur="2s" repeatCount="indefinite"/>
  </text>
  <text x="40" y="35" class="heavy">
    cat
    <animateTransform attributeName="transform" type="translate" from="0 0" to="0 0.5" dur="2s" repeatCount="indefinite"/>
  </text>
  <text x="55" y="55" class="small">
    is
    <animateTransform attributeName="transform" type="translate" from="0 0" to="0.5 0.5" dur="2s" repeatCount="indefinite"/>
  </text>
  <text x="65" y="55" class="Rrrrr">
    Grumpy!
        <animateTransform attributeName="transform" type="translate" from="1 1" to="0 0" dur="2s" repeatCount="indefinite"/>
  </text>

It may also be desirable to optimize simple translations when, e.g., reacting to scroll events (like with TouchArea's scroll-event). Theoretically, when implementing a virtual scroller and translating the content block, the for-rendered elements that didn't change wouldn't need to be rerendered, if the remainder of a modulo calculation on translation as an f32 is less than a certain small amount whose calculation involves the bit depth (like 8 bits per pixel, meaning 256 subpixel fractions; also involving the height of the image data to be translated?). Then, the implementor of the virtual scroller can ensure scroll translation floats that aim for whole-physical-pixel translations to easily trigger this rendering optimization. This may also make animated scrolling with many intermediate translations more fluent (although, would the animation system need to support the intermediate translations to only be in whole physical pixels?).