Skip to content
This repository has been archived by the owner on Jul 15, 2024. It is now read-only.

Make use of physical and logical size types #70

Open
senneh opened this issue Feb 13, 2023 · 7 comments
Open

Make use of physical and logical size types #70

senneh opened this issue Feb 13, 2023 · 7 comments

Comments

@senneh
Copy link

senneh commented Feb 13, 2023

It isn't always clear whether a size is in pixel or display points. Better documentation could help, but making use of the type system would be even better.

I think there are 2 ways of going about this. Using wrapper types

pub struct Physical<S: Scalable> {
    pub pixels: S,
}

pub struct Logical<S: Scalable> {
    pub points: S,
}

Or using an enum. This is how winit does it

pub enum Dip<S: Scalable> {
    Pixels(S),
    Points(S),
}
@jneem
Copy link
Collaborator

jneem commented Feb 14, 2023

Can you say something about the advantages/disadvantages of the two representations?

@dhardy
Copy link
Contributor

dhardy commented Feb 15, 2023

For outputs, e.g. window.get_size(), it's not necessary to use an enum.

For inputs, e.g. window.set_size(size), use of an enum is roughly equivalent to providing two variants of the method. Note that one needs to know the scale factor to convert, but a window should know that...

...but note there are some restrictions, e.g. on Wayland the initial window size must be specified in logical pixels (the scale factor might be guessable but should be treated as unknown). Meanwhile, X11 is the other way around: the size must be specified in physical pixels (but, since the scale factor must be the same for all screens, you should at least be able to guess that and convert from logical pixels).

@xStrom
Copy link
Member

xStrom commented Feb 15, 2023

I think a leaner less complex API is superior. We should use logical everywhere in the API except places where it won't work. (So basically how the current API works.) Then the inner implementation can worry about how to translate the platform API.

From that it follows that an enum isn't ideal, because yes it'll mandate two paths for every method.

Additionally, we want to do the conversion the minimum number of times possible, because of floating point math error accumulation and pixel alignment. Doing as much as possible with logical units gives us the power to choose where and how many times the conversion happens.

Also, ergonomics is important. Given the thinking that logical units should be used whenever possible, it would be a lot more ergonomical to just define a convention that a bare Point / Size in the API is always logical. Then have the wrapper type (or something similar) for the rare cases where physical units are used instead. This would remove the need for a lot of unwrapping, while still getting the type system to complain when the units are incorrectly used at a crossing point.

@jneem
Copy link
Collaborator

jneem commented Feb 15, 2023

That's pretty convincing; I'm on board with doing the statically typed version instead of the enum.

There's one situation that I'm still a little concerned about (that Druid currently gets wrong): although we mostly want to use logical units, we do often want to round layout to pixel boundaries in order to ensure, for example, that 1-pixel-width lines look nice. Having a bare Point / Size mean logical size makes it very tempting to just round those numbers.

@xStrom xStrom changed the title Make use of physical a logical size types Make use of physical and logical size types Feb 15, 2023
@xStrom
Copy link
Member

xStrom commented Feb 16, 2023

Yes alignment is tricky. Druid has a partial solution, but it fails some cases and doesn't even attempt dynamic fractional scale alignment [1]. I've been thinking about this on and off, but haven't found an ultimate solution yet. It would probably be wise to write up some thoughts and get some discussion going that would result in a document describing what we would like to happen in all the various cases. For now, here are some rough thoughts that immediately come to mind.

There's the simple case of 1x and 2x scaling. Just rounding the logical units will go very far to solve the problem here.

Then there's the extremely common 1.25x, 1.75x, and friends. Just rounding logical units won't work too well. Containers could probably use scale info to perform the rounding based on physical integers. That way at least every container's origin would be aligned and thus the container could do alignment of children with just the scale info and not having to know its window coords.

[1] It gets trickier though. Because yes what about things like thin lines. Perhaps a dynamic size would be interesting. SharpLine if you will. The logical width will depend on scale to ensure that it is always super sharp in terms of physical pixels. Although to ensure usage it would have to be very ergonomic. Perhaps some default trait methods on kurbo shapes like expand/round/shrink_to_physical_pixels(scale), but with better names. Plenty of details to figure out there.

Sometimes you want the half-transparent interpolation and other times you want sharp alignment. Should probably try to gather some use case scenarios and figure out if there is a strong bias either way, because that's what the default should be.

@senneh
Copy link
Author

senneh commented Feb 16, 2023

I agree that I'm not a fan of the enum option either.
The only place I can think of where we might want to give users a choice of what to pass would be at window creation, since you don't know the scale yet at that point. Depending on the content you want to show you might prefer a certain physical size over a size that's consistent on many screens.
Though that's not impossible to work around by making your window invisible first and resizing if necessary.

alignment

But alignment isn't really glazier's responsibility, right? It's the responsibility of a UI toolkit built on top. (Unless you're talking about the sub compositor). I just need to know the physical size of the buffer I'm drawing on.

For a UI toolkit, allowing mixed units makes more sense: "I want a button that's 60 points wide but with a 1 pixel border".
Whether that button width gets rounded up or down by one pixel doesn't matter, but losing that pixel border does.

Sometimes you want the half-transparent interpolation

Wouldn't you want sharp alignment in almost all cases for a gui?

@dhardy
Copy link
Contributor

dhardy commented Feb 16, 2023

We should use logical everywhere in the API except places where it won't work.

Sounds good. A couple of exceptions I see:

  1. It should be possible to get the window size (sans decorations, or whatever the user outputs) in physical pixels.
  2. Decorations: this is an issue whenever not provided by the window manager (e.g. on Gnome). If Glazier draws decorations (like winit now does), then you probably need to make sure they are a whole number of physical pixels in size for proper alignment of the contents.

for example, that 1-pixel-width lines look nice

Yes alignment is tricky. Druid has a partial solution, but it fails some cases and doesn't even attempt dynamic fractional scale alignment [1].

This is getting way off-topic, but I basically solved it. E.g. see Dimensions::new: it uses cast_nearest() everywhere to convert sizes to whole numbers of physical pixels, then uses physical pixels everywhere in widget code to ensure alignment. The caveats are that widget code must pull sizes from the theme's Dimensions type for most stuff (arguably useful anyway) and remember to scale if using hard-coded sizes.

But yes, this has nothing to do with Glazier (or Winit).

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants