guido 0.12.0

A reactive Rust GUI library using wgpu for Wayland layer shell widgets
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# 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`,
`line_height`, `letter_spacing`, `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.

A weight the family has no face for is drawn in the family's nearest one, as
CSS Fonts 4 §5.2 matches it, and never in another family that happens to have
it. For a generic family the family is the installed one that draws it — a
fallback where cosmic-text's own name for it is not installed — and a variable
face is drawn at the weight asked, on its `wght` axis (`resolve_font` in
`src/renderer/text.rs`).

`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.

`letter_spacing` is extra advance in logical pixels, independent of the font
size — `text("LABEL").letter_spacing(1.5)` — and `0.0`, the default, is the
font's own spacing; a negative value tightens it and a value that is not a
number is `0.0`. It is resolved beside the line height, in `shape`, which takes
it to physical pixels by the scale and hands it to cosmic-text as the fraction
of the size `Attrs::letter_spacing` asks for. What it is added to is
cosmic-text's decision, not ours: the advance of every glyph it shapes, the
last on a line included. So a ligature takes one spacing for the letters it
joins, a combining mark the shaper cannot compose into its base is a glyph of
its own and takes one too — landing that far right of the base — and joined
Arabic letters are pulled apart with no connection kept across the gap. The
tests that pin this are in `src/renderer/text_measurer.rs`, and
`letter_spacing_reaches_every_path` in `tests/golden_images.rs` draws it.
A `TextInput` places its caret, its selection and its clicks from the same
spaced shaping, and a `PasswordInput` spaces the mask characters it draws.

`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))),
        ])
}
```