# 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
### 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`,
`line_height`, `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.
`line_height` defaults to `LineHeight::Normal`: the ascent, descent and line
gap of the face that draws the family at that weight, which is a fallback face
where the family's own font is not installed. A bare number is
`LineHeight::Relative`, a factor of the font size —
`text("Notes").line_height(1.5)` — and `LineHeight::Absolute` is logical
pixels. It is resolved in one place, `shape` in `src/renderer/text.rs`, which
the measurer and all three draw paths call. `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
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))),
])
}
```