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-like Border #4019

Open
Tracked by #3284
emilk opened this issue Feb 10, 2024 · 1 comment · May be fixed by #4294
Open
Tracked by #3284

CSS-like Border #4019

emilk opened this issue Feb 10, 2024 · 1 comment · May be fixed by #4294
Labels

Comments

@emilk
Copy link
Owner

emilk commented Feb 10, 2024

As a first step towards more powerful styling, I want to change how Frame works to be slightly more like the CSS box model.

In particular, the frame border stroke width should be counted as part of the width of the frame. This should let us get rid of the ugly clip_rect_margin.

The plan is to use the new Frame for essentially all widgets (except maybe Label). So a Button would ask the Style for a Frame, then use that for both its sizing and color calculations.

Here is the proposed outline

/// A frame around some content, including margin and border colors.
/// 
/// The total (outer) size of a frame is
/// `content_size + inner_margin + border.stroke.width + outer_margin`.
/// 
/// Everything within the border stroke is filled with the border fill color (if any).
/// 
/// ```
/// +---------------^-------------------------------+
/// |               | outer_margin                  |
/// |   +-----------v----^-----------------------+  |
/// |   |                | border stroke width   |  |
/// |   |  +-------------v---^----------------+  |  |
/// |   |  |                 | inner_margin   |  |  |
/// |   |  |  +--------------v-------------+  |  |  |
/// |   |  |  |                            |  |  |  |
/// |   |  |  |                            |  |  |  |
/// |   |  |  | content rect               |  |  |  |
/// |   |  |  +----------------------------+  |  |  |
/// |   |  |                                  |  |  |
/// |   |  +----------------------------------+  |  |
/// |   |       widget rect                      |  |
/// |   +----------------------------------------+  |
/// |           outer rect                          |
/// +-----------------------------------------------+
/// ```
/// 
/// `widget rect` is the interactive part of the widget (what sense clicks etc),
/// and is what is returned by [`Response::rect`].
struct Frame {
    /// Inner spacing. Called `padding` in CSS.
    pub inner_margin: Margin,

    /// Stroke, fill color etc.
    ///
    /// The width of the border is counted as part of the size of the frame.
    pub border: Border,

    /// Optional drop-shadow.
    /// Does not affect margins or sizes.
    pub shadow: Shadow,

    /// Outer spacing. Called `margin` in CSS.
    /// 
    /// This does NOT do "Margin Collapse" like in CSS,
    /// i.e. when placing two frames next to each other,
    /// the distance between their borders is the SUM
    /// of their other margin.
    /// In CSS the distance would be the MAX of their outer margins.
    ///
    /// Supporting margin collapse is difficult, and would 
    /// requires complicating the already complicated egui layout code.
    /// 
    /// Consider using [`egui::Spacing::item_spacing`] instead
    /// for adding space between widgets.
    pub outer_margin:  Margin,

    /// The inner rectangle must be at least this size.
    pub min_size: Vec2,
}

/// Stroke, fill, rounding, etc.
/// 
/// Controls the look of widgets and [`RectShape`].
///
/// Similar to a CSS border.
struct Border {
    /// Fills the content and inner padding of a [`Frame`].
    ///
    /// In CSS this is called `background`.
    pub fill: Color32,

    pub stroke: Stroke,

    pub rounding: Rounding,

    // TODO: add texturing here eventually
}

struct RectShape {
    /// The rectangle on which edge the border stroke is painted.
    pub rect: Rect,

    pub border: Border,

    // TODO: consider moving these into `Border`.
    // Care must be taken so that `Border` and `Frame` doesn't become too huge in common cases,
    // i.e. when there is no texture, and the rounding is uniform.
    pub fill_texture_id: TextureId,
    pub uv: Rect,
}
@emilk emilk added the egui label Feb 10, 2024
@emilk emilk mentioned this issue Feb 10, 2024
8 tasks
@YgorSouza
Copy link
Contributor

In WPF/Avalonia there is a type called Brush, which is used for the foreground, background, and stroke properties on the widgets. It can be a solid color, a gradient, a texture etc. In .NET this is done through polymorphism everywhere (every class in WPF has like 9 abstract parent classes), but maybe egui could do the same with an enum? We would still be packing 24 bytes per field instead of 4 (if every variant other than Color32 is boxed), but maybe it would be worth it for the flexibility.

The extra variants could also be behind a feature, so the size becomes 4 bytes when it is not active. But then it has to be marked #[non_exhaustive], otherwise enabling the feature would cause a SemVer breaking change, and that goes against the API guidelines since features have to be additive.

Repository owner deleted a comment from rustbasic Apr 1, 2024
@emilk emilk linked a pull request Jul 31, 2024 that will close this issue
emilk pushed a commit that referenced this issue Aug 30, 2024
<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->


* Closes <#4776>
* [x] I have followed the instructions in the PR template



I've been meaning to look into this for a while but finally bit the
bullet this week. Contrary to what I initially thought, the problem of
blurry lines is unrelated to feathering because it also happens with
feathering disabled.

The root cause is that lines tend to land on pixel boundaries, and
because of that, frequently used strokes (e.g. 1pt), end up partially
covering pixels. This is especially noticeable on 1ppp displays.

There were a couple of things to fix, namely: individual lines like
separators and indents but also shape strokes (e.g. Frame).

Lines were easy, I just made sure we round them to the nearest pixel
_center_, instead of the nearest pixel boundary.

Strokes were a little more complicated. To illustrate why, here’s an
example: if we're rendering a 5x5 rect (black fill, red stroke), we
would expect to see something like this:

![Screenshot 2024-08-11 at 15 01
41](https://github.com/user-attachments/assets/5a5d4434-0814-451b-8179-2864dc73c6a6)

The fill and the stroke to cover entire pixels. Instead, egui was
painting the stroke partially inside and partially outside, centered
around the shape’s path (blue line):

![Screenshot 2024-08-11 at 15 00
57](https://github.com/user-attachments/assets/4284dc91-5b6e-4422-994a-17d527a6f13b)

Both methods are valid for different use-cases but the first one is what
we’d typically want for UIs to feel crisp and pixel perfect. It's also
how CSS borders work (related to #4019 and #3284).

Luckily, we can use the normal computed for each `PathPoint` to adjust
the location of the stroke to be outside, inside, or in the middle.
These also are the 3 types of strokes available in tools like Photoshop.

This PR introduces an enum `StrokeKind` which determines if a
`PathStroke` should be tessellated outside, inside, or _on_ the path
itself. Where "outside" is defined by the directions normals point to.

Tessellator will now use `StrokeKind::Outside` for closed shapes like
rect, ellipse, etc. And `StrokeKind::Middle` for the rest since there's
no meaningful "outside" concept for open paths. This PR doesn't expose
`StrokeKind` to user-land, but we can implement that later so that users
can render shapes and decide where to place the stroke.

### Strokes test
(blue lines represent the size of the rect being rendered)

`Stroke::Middle` (current behavior, 1px and 3px are blurry)
![Screenshot 2024-08-09 at 23 55
48](https://github.com/user-attachments/assets/dabeaa9e-2010-4eb6-bd7e-b9cb3660542e)


`Stroke::Outside` (proposed default behavior for closed paths)
![Screenshot 2024-08-09 at 23 51
55](https://github.com/user-attachments/assets/509c261f-0ae1-46a0-b9b8-08de31c3bd85)



`Stroke::Inside` (for completeness but unused at the moment)
![Screenshot 2024-08-09 at 23 54
49](https://github.com/user-attachments/assets/c011b1c1-60ab-4577-baa9-14c36267438a)



### Demo App
The best way to review this PR is to run the demo on a 1ppp display,
especially to test hover effects. Everything should look crisper. Also
run it in a higher dpi screen to test that nothing broke 🙏.

Before:

![egui_old](https://github.com/user-attachments/assets/cd6e9032-d44f-4cb0-bb41-f9eb4c3ae810)


After (notice the sharper lines):

![egui_new](https://github.com/user-attachments/assets/3365fc96-6eb2-4e7d-a2f5-b4712625a702)
486c pushed a commit to 486c/egui that referenced this issue Oct 9, 2024
<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->


* Closes <emilk#4776>
* [x] I have followed the instructions in the PR template



I've been meaning to look into this for a while but finally bit the
bullet this week. Contrary to what I initially thought, the problem of
blurry lines is unrelated to feathering because it also happens with
feathering disabled.

The root cause is that lines tend to land on pixel boundaries, and
because of that, frequently used strokes (e.g. 1pt), end up partially
covering pixels. This is especially noticeable on 1ppp displays.

There were a couple of things to fix, namely: individual lines like
separators and indents but also shape strokes (e.g. Frame).

Lines were easy, I just made sure we round them to the nearest pixel
_center_, instead of the nearest pixel boundary.

Strokes were a little more complicated. To illustrate why, here’s an
example: if we're rendering a 5x5 rect (black fill, red stroke), we
would expect to see something like this:

![Screenshot 2024-08-11 at 15 01
41](https://github.com/user-attachments/assets/5a5d4434-0814-451b-8179-2864dc73c6a6)

The fill and the stroke to cover entire pixels. Instead, egui was
painting the stroke partially inside and partially outside, centered
around the shape’s path (blue line):

![Screenshot 2024-08-11 at 15 00
57](https://github.com/user-attachments/assets/4284dc91-5b6e-4422-994a-17d527a6f13b)

Both methods are valid for different use-cases but the first one is what
we’d typically want for UIs to feel crisp and pixel perfect. It's also
how CSS borders work (related to emilk#4019 and emilk#3284).

Luckily, we can use the normal computed for each `PathPoint` to adjust
the location of the stroke to be outside, inside, or in the middle.
These also are the 3 types of strokes available in tools like Photoshop.

This PR introduces an enum `StrokeKind` which determines if a
`PathStroke` should be tessellated outside, inside, or _on_ the path
itself. Where "outside" is defined by the directions normals point to.

Tessellator will now use `StrokeKind::Outside` for closed shapes like
rect, ellipse, etc. And `StrokeKind::Middle` for the rest since there's
no meaningful "outside" concept for open paths. This PR doesn't expose
`StrokeKind` to user-land, but we can implement that later so that users
can render shapes and decide where to place the stroke.

### Strokes test
(blue lines represent the size of the rect being rendered)

`Stroke::Middle` (current behavior, 1px and 3px are blurry)
![Screenshot 2024-08-09 at 23 55
48](https://github.com/user-attachments/assets/dabeaa9e-2010-4eb6-bd7e-b9cb3660542e)


`Stroke::Outside` (proposed default behavior for closed paths)
![Screenshot 2024-08-09 at 23 51
55](https://github.com/user-attachments/assets/509c261f-0ae1-46a0-b9b8-08de31c3bd85)



`Stroke::Inside` (for completeness but unused at the moment)
![Screenshot 2024-08-09 at 23 54
49](https://github.com/user-attachments/assets/c011b1c1-60ab-4577-baa9-14c36267438a)



### Demo App
The best way to review this PR is to run the demo on a 1ppp display,
especially to test hover effects. Everything should look crisper. Also
run it in a higher dpi screen to test that nothing broke 🙏.

Before:

![egui_old](https://github.com/user-attachments/assets/cd6e9032-d44f-4cb0-bb41-f9eb4c3ae810)


After (notice the sharper lines):

![egui_new](https://github.com/user-attachments/assets/3365fc96-6eb2-4e7d-a2f5-b4712625a702)
hacknus pushed a commit to hacknus/egui that referenced this issue Oct 30, 2024
<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->


* Closes <emilk#4776>
* [x] I have followed the instructions in the PR template



I've been meaning to look into this for a while but finally bit the
bullet this week. Contrary to what I initially thought, the problem of
blurry lines is unrelated to feathering because it also happens with
feathering disabled.

The root cause is that lines tend to land on pixel boundaries, and
because of that, frequently used strokes (e.g. 1pt), end up partially
covering pixels. This is especially noticeable on 1ppp displays.

There were a couple of things to fix, namely: individual lines like
separators and indents but also shape strokes (e.g. Frame).

Lines were easy, I just made sure we round them to the nearest pixel
_center_, instead of the nearest pixel boundary.

Strokes were a little more complicated. To illustrate why, here’s an
example: if we're rendering a 5x5 rect (black fill, red stroke), we
would expect to see something like this:

![Screenshot 2024-08-11 at 15 01
41](https://github.com/user-attachments/assets/5a5d4434-0814-451b-8179-2864dc73c6a6)

The fill and the stroke to cover entire pixels. Instead, egui was
painting the stroke partially inside and partially outside, centered
around the shape’s path (blue line):

![Screenshot 2024-08-11 at 15 00
57](https://github.com/user-attachments/assets/4284dc91-5b6e-4422-994a-17d527a6f13b)

Both methods are valid for different use-cases but the first one is what
we’d typically want for UIs to feel crisp and pixel perfect. It's also
how CSS borders work (related to emilk#4019 and emilk#3284).

Luckily, we can use the normal computed for each `PathPoint` to adjust
the location of the stroke to be outside, inside, or in the middle.
These also are the 3 types of strokes available in tools like Photoshop.

This PR introduces an enum `StrokeKind` which determines if a
`PathStroke` should be tessellated outside, inside, or _on_ the path
itself. Where "outside" is defined by the directions normals point to.

Tessellator will now use `StrokeKind::Outside` for closed shapes like
rect, ellipse, etc. And `StrokeKind::Middle` for the rest since there's
no meaningful "outside" concept for open paths. This PR doesn't expose
`StrokeKind` to user-land, but we can implement that later so that users
can render shapes and decide where to place the stroke.

### Strokes test
(blue lines represent the size of the rect being rendered)

`Stroke::Middle` (current behavior, 1px and 3px are blurry)
![Screenshot 2024-08-09 at 23 55
48](https://github.com/user-attachments/assets/dabeaa9e-2010-4eb6-bd7e-b9cb3660542e)


`Stroke::Outside` (proposed default behavior for closed paths)
![Screenshot 2024-08-09 at 23 51
55](https://github.com/user-attachments/assets/509c261f-0ae1-46a0-b9b8-08de31c3bd85)



`Stroke::Inside` (for completeness but unused at the moment)
![Screenshot 2024-08-09 at 23 54
49](https://github.com/user-attachments/assets/c011b1c1-60ab-4577-baa9-14c36267438a)



### Demo App
The best way to review this PR is to run the demo on a 1ppp display,
especially to test hover effects. Everything should look crisper. Also
run it in a higher dpi screen to test that nothing broke 🙏.

Before:

![egui_old](https://github.com/user-attachments/assets/cd6e9032-d44f-4cb0-bb41-f9eb4c3ae810)


After (notice the sharper lines):

![egui_new](https://github.com/user-attachments/assets/3365fc96-6eb2-4e7d-a2f5-b4712625a702)
@emilk emilk added this to egui Nov 13, 2024
@emilk emilk moved this to Backlog in egui Nov 13, 2024
@emilk emilk moved this from Backlog to Ready in egui Dec 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Next up
Development

Successfully merging a pull request may close this issue.

2 participants