emilk / egui

egui: an easy-to-use immediate mode GUI in Rust that runs on both web and native
https://www.egui.rs/
Apache License 2.0
22.17k stars 1.6k forks source link

CSS-like `Border` #4019

Open emilk opened 8 months ago

emilk commented 8 months ago

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,
}
YgorSouza commented 8 months ago

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.