guido 0.8.0

A reactive Rust GUI library using wgpu for Wayland layer shell widgets
Documentation
# Styling Guide

This document covers the visual styling options available in Guido.

## Colors

### Creating Colors

```rust
// RGB (values 0.0-1.0)
Color::rgb(0.2, 0.4, 0.8)

// RGBA with alpha
Color::rgba(0.2, 0.4, 0.8, 0.5)

// Predefined colors
Color::WHITE
Color::BLACK
Color::RED
Color::GREEN
Color::BLUE
Color::TRANSPARENT
```

### Color Operations

```rust
// Blend toward white (lighter)
let lighter = color.lighter(0.1);  // 10% lighter

// Blend toward black (darker)
let darker = color.darker(0.1);   // 10% darker
```

## Backgrounds

### Solid Color

```rust
container().background(Color::rgb(0.2, 0.2, 0.3))
```

### Gradients

```rust
// Horizontal gradient (left to right)
container().gradient(LinearGradient::horizontal(Color::RED, Color::BLUE))

// Vertical gradient (top to bottom)
container().gradient(LinearGradient::vertical(Color::RED, Color::BLUE))

// Diagonal gradient
container().gradient(LinearGradient::diagonal(Color::RED, Color::BLUE))
```

## Borders

### Basic Border

```rust
container()
    .border(2.0, Color::WHITE)  // 2px white border
```

A border is always both halves at once — on the container and in a state layer
alike. A width with no colour and a colour with no width are the same thing, no
border, so there is nothing for a half-declaration to mean and no way to write
one. Each half takes a signal of its own, which covers anything that has to
change:

```rust
container().border(1.5, move || if failed.get() { theme.danger } else { theme.line })
```

### In a State Layer

Same call, and it replaces the whole border. Layers resolve last-declared-wins,
so the border a caller wants to outrank the others goes last:

```rust
container()
    .border(1.5, theme.line)
    .when_focused(|s| s.border(1.5, theme.accent))
    .state(failed, |s| s.border(1.5, theme.danger))
```

A width repeated across layers is a constant in your own code, not something the
API should let you leave half-said:

```rust
const FIELD_BORDER: f32 = 1.5;
```

### Animated Borders

```rust
container()
    .border(1.0.transition(150.0), Color::rgb(0.3, 0.3, 0.4).transition(150.0))
    .when_hovered(|s| s.border(2.0, Color::rgb(0.5, 0.5, 0.6)))
```

## Corner Radius

One call, one to four values — the shorthand `padding` uses, with CSS's
clockwise order where CSS has one:

```rust
container().corners(8.0)                     // all four
container().corners([16.0, 0.0])             // the top pair, then the bottom
container().corners([16.0, 4.0, 16.0, 4.0])  // top-left, top-right, bottom-right, bottom-left
```

The two-value form is `[top, bottom]` rather than CSS's diagonal pairing
(`top-left & bottom-right`, then the other two), which is a curiosity almost
nobody writes on purpose; what people write is "round the top of this row".
Note that it does not mirror `padding([a, b])`, which pairs *opposite sides*
(vertical, then horizontal) — corners have no opposite pairs to give, so the
two-value form pairs adjacent ones. The four-value form *is* CSS, clockwise
from the top left.

Named constructors say the same thing in words:

```rust
container().corners(CornerRadii::top(16.0))     // first row of a list
container().corners(CornerRadii::bottom(16.0))  // last row
```

The shape reaches everything that has one: the background, the border, the
shadow, the gradient, the blur behind it, the clip its children are cut to,
and the region that answers a click. A box rounded only at the top clips and
is clicked as a box rounded only at the top.

### The shape of the corner

A constructor names the shape and takes the size. The curve is a CSS K-value:
how far from the corner the arc starts.

```rust
container().corners(Corners::squircle(12.0))          // K=2, iOS-style
container().corners(Corners::rounded(12.0))           // K=1, circular (default)
container().corners(Corners::bevel(12.0))             // K=0, diagonal cut
container().corners(Corners::scoop(12.0))             // K=-1, concave
container().corners(Corners::superellipse(12.0, 1.5)) // anything between
```

## Shadows

One property, four degrees of freedom: offset, blur, spread and colour.

```rust
container().shadow(Shadow::new((0.0, 2.0), 4.0, 0.0, Color::rgba(0.0, 0.0, 0.0, 0.16)))
container().shadow(Shadow::simple((0.0, 8.0), 16.0, Color::rgba(0.0, 0.0, 0.0, 0.22)))
container().shadow(Shadow::none())
```

There is no elevation level and no table behind one. A design system's ladder is
a set of `Shadow` constants the application writes down, the way it writes down
its colours — `Shadow::new`, `Shadow::simple` and `Shadow::none` are all `const`
so they cost nothing:

```rust
mod elevation {
    use guido::prelude::{Color, Shadow};

    const fn step(offset_y: f32, blur: f32, alpha: f32) -> Shadow {
        Shadow::new((0.0, offset_y), blur, 0.0, Color::rgba(0.0, 0.0, 0.0, alpha))
    }

    pub const FLAT: Shadow = Shadow::none();
    pub const LOW: Shadow = step(1.0, 3.0, 0.12);
    pub const RAISED: Shadow = step(3.0, 6.0, 0.19);
    pub const HIGH: Shadow = step(6.0, 10.0, 0.22);
}
```

`examples/shadow_example.rs` shows that ladder beside the three things one
number could never say: a shadow thrown sideways, a coloured one, and one with
spread.

### Shadows in State Layers

```rust
container()
    .shadow(elevation::LOW.transition(160.0))
    .when_hovered(|s| s.shadow(elevation::RAISED))
    .when_pressed(|s| s.shadow(elevation::FLAT))
```

## Padding

```rust
container().padding(16.0)                          // 16px on all sides
container().padding(16)                            // integers work too
container().padding([8.0, 16.0])                   // [vertical, horizontal]
container().padding([8, 16])                       // integer arrays work too
container().padding([1.0, 2.0, 3.0, 4.0])         // [top, right, bottom, left]
container().padding([1, 2, 3, 4])                  // integer 4-value shorthand
container().padding(Padding::all(8.0).with_top(20.0))   // builder pattern
```

## Sizing

### Fixed Size

```rust
container()
    .width(100.0)
    .height(50.0)

// Integers work too
container()
    .width(100)
    .height(50)
```

### Minimum/Maximum Size

```rust
container()
    .width(at_least(50.0).at_most(200.0))
    .height(at_least(30.0).at_most(100.0))
```

## Static or Reactive

Every property that survives to paint takes a signal, a closure, or a plain
value — the same `IntoSignal` shape everywhere, so which spelling you use is
never the API's decision:

```rust
container()
    .background(theme.surface)                       // a constant
    .background(surface_color)                       // a signal
    .background(move || if hot.get() { HOT } else { COOL })   // a closure
    .gradient(move || palette.get().header())
    .backdrop_blur(move || if frosted.get() { 24.0 } else { 0.0 })
    .overflow(move || if collapsed.get() { Overflow::Hidden } else { Overflow::Visible })
```

A blur radius of `0.0` is "no blur", on a container and on a text alike, which
is what lets one signal switch the effect on and off rather than forcing the
caller to rebuild the widget in a Rust branch. That holds wherever the surface
is one of the sources, which is everything by default. A container that
restricts itself to the compositor has no radius of its own — the protocol
carries none — so it asks on its sources alone, and empties them to switch off.

**What is not reactive** is structural: `.layout(..)`, the axis a `.scroll(..)`
is built with, `.control()`, and the motion a value is declared with —
`.transition(..)`, `.timeline(..)`, the `.entering_from(..)` that says where
a transition begins on the frame the widget appears, and the `.exiting_to(..)`
that says where it ends when the widget is removed — the one of them read at
removal rather than at build, since that is when the direction is known. These
say what
kind of thing the container *is*; change one and you are describing a different
widget, so declare it in the closure that builds the widget instead. The
*value* a motion decorates is as reactive as any other.

## Text Styling

How a text looks can be declared on the text itself:

```rust
text("Hello").font_size(16.0).color(Color::WHITE)
```

`color`, `font_size`, `font_family`, `font_weight`, `bold`, `mono`,
`text_stroke` and `text_shadow` are declared on `Text` and `TextInput` — the two
widgets that draw glyphs — from one list, so the two cannot drift apart.
`color` and `font_size` are declarations, so they carry how they move as well
as what they are: `color(theme.warn.transition(200.0))`. The rest take values,
because a family and a weight snap to an installed face and a stroke and a
shadow have nothing to interpolate. `cursor_color`, `selection_color` and
`placeholder_color` are `TextInput`'s own methods and nothing else's, because
it is the only widget that draws a caret, a band or a placeholder.

### The same style, many times: write a function

There is no inheritance: a container declares nothing about the text inside
it, because a container draws a box. For "the same kind of label, many times",
the tool is the one the language already gives you:

```rust
let label = |s: &str| text(s).color(theme.weak).font_size(12.0);

container()
    .layout(Flex::row().spacing(8.0))
    .children([label("one"), label("two"), label("three")])
```

That keeps the declaration next to the widget that draws it, costs no wrapper
node, and gives the style a name.

### A state that reaches the glyphs

Hover belongs to the box and colour to the glyphs, so each is declared where it
happens and [`control()`](../docs/STATE_LAYER.md) joins them: the leaf resolves
its own states from the nearest control above it.

```rust
container()
    .padding(8.0)
    .control()
    .when_hovered(|s| s.lighter(0.1))
    .child(
        text("Label")
            .color(theme.weak)
            .when_hovered(|s| s.color(theme.strong)),
    )
```

### Properties with no declaration

Fall back to white, 14 logical pixels, the registered default family and normal
weight.

### Stroke and shadow

For text over an image, where no single colour works against the whole
picture:

```rust
container()
    
    
    
    .child(text("09:41").text_shadow(TextShadow::new(0.0, 2.0, 10.0, Color::rgba(0.0, 0.0, 0.0, 0.75))).text_stroke(TextStroke::new(1.5, Color::BLACK)).color(Color::WHITE))
```

Both are drawn under the fill — a stroke painted over it would eat half the
weight of every stem. Neither affects layout.

## Layout Styling

### Flex Layout

```rust
container()
    .layout(Flex::row().spacing(8.0).center())
```

### Alignment Options

**Main Axis (direction of flow):**
- `MainAlignment::Start`
- `MainAlignment::End`
- `MainAlignment::Center`
- `MainAlignment::SpaceBetween`
- `MainAlignment::SpaceAround`
- `MainAlignment::SpaceEvenly`

**Cross Axis (perpendicular to flow):**
- `CrossAlignment::Start`
- `CrossAlignment::End`
- `CrossAlignment::Center`
- `CrossAlignment::Stretch`

## Complete Example

```rust
fn styled_card(title: &str, content: &str) -> Container {
    container()
        // Size and padding
        .width(300.0)
        .padding(20.0)
        // Background and corners — the background eases, so the hover below
        // arrives over 200ms rather than in one frame
        .background(Color::rgb(0.15, 0.15, 0.2).transition(200.0))
        .corners(Corners::squircle(12.0))
        
        // Border
        .border(1.0, Color::rgb(0.25, 0.25, 0.3))
        // Shadow
        .shadow(Shadow::simple((0.0, 4.0), 8.0, Color::rgba(0.0, 0.0, 0.0, 0.2)))
        // Layout
        .layout(Flex::column().spacing(12.0))
        // State layers — they supply values; the motion is declared above
        .when_hovered(|s| {
            s.lighter(0.05)
                .shadow(Shadow::simple((0.0, 6.0), 12.0, Color::rgba(0.0, 0.0, 0.0, 0.24)))
        })
        // Children
        .children([
            container().child(text(title).font_size(18.0).color(Color::WHITE)),
            container().child(text(content).font_size(14.0).color(Color::rgb(0.7, 0.7, 0.75))),
        ])
}
```