guido 0.7.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
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
# Text

The Text widget renders text content with support for reactive updates.

## Basic Text

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
text("Hello, World!")
# ;
# }
```

## Styling

A text declares how it looks:

```rust
# extern crate guido;
# use guido::prelude::*;
# #[derive(Clone, Copy)]
# struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
# impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
# fn main() {
# let theme = Theme::default();
text("Hello").font_size(24.0).color(theme.text)
# ;
# }
```

The methods — `color`, `font_size`, `font_family`, `font_weight`, `bold`,
`mono`, `text_stroke`, `text_shadow` — belong to the two widgets that draw
glyphs, `Text` and `TextInput`, and are written from one list so the two always
offer the same thing.

`color` and `font_size` are *declarations*, so they say how they move as well as
what they are:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
# let busy = create_signal(false);
text("saving…").color((move || if busy.get() { Color::RED } else { Color::WHITE })
    .transition(200.0))
# ;
# }
```

A state override supplies a value and never a timing — `when_hovered(|s|
s.color(..))` takes a colour alone, and a transition there is a compile error
rather than something quietly ignored.

### Repeating a style

A container declares nothing about the text inside it: it draws a box, and the
widget that draws the glyphs is the one that says how they look. For "the same
kind of label, many times", write a function:

```rust
# extern crate guido;
# use guido::prelude::*;
# #[derive(Clone, Copy)]
# struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
# impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
# fn main() {
# let theme = Theme::default();
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")])
# ;
# }
```

The style keeps a name, stays next to the widget that draws it, and costs no
extra node.

Properties nothing declares fall back to white, 14 logical pixels, the
registered default family and normal weight.

### A hover that reaches the glyphs

The pointer is over the *box*, and the colour belongs to the *glyphs*. Declare
each where it happens, and let `control()` join them — the text resolves its
own states from the nearest control above it:

```rust
# extern crate guido;
# use guido::prelude::*;
# #[derive(Clone, Copy)]
# struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
# impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
# fn main() {
# let theme = Theme::default();
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)),
    )
# ;
# }
```

## Legibility over an image

Over a photograph no single text colour works, because the picture is light in
some places and dark in others. Two declarations separate the glyphs from
whatever is behind them:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
container()
    
    // A contour around the glyphs. CSS spells this `-webkit-text-stroke`;
    // CSS `outline` is the contour of a box, which is why the name differs.
    
    // As CSS `text-shadow`: offset x, offset y, blur, colour.
    
    .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))
# ;
# }
```

The shadow is usually the more effective of the two: it darkens the whole
neighbourhood the glyph sits in rather than only its edge. Both are drawn
*under* the fill, so a stroke outlines the text instead of eating into it.

Both are approximated by re-drawing the glyphs at offsets, which costs fill rate
but no extra rasterization — the glyph atlas is keyed on glyph, size and weight,
and takes colour per draw. What decides whether the approximation shows is the
*spacing* between copies: more than a couple of pixels apart and they stop
blending, so the square features of a glyph — a colon's dots, the stem of a 4 —
read as separate copies rather than one halo. The shadow therefore fills a disc
whose sample count grows with the radius (about a hundred copies at blur 10), and
a very wide blur spreads a fixed budget thinner instead of costing more. A thick
stroke is the case with a visible ceiling: its corners begin to scallop past a
few pixels, where the honest fix is a dilate on an offscreen mask, not more taps.

Neither changes how much room the text takes, so adding a shadow never moves
its neighbours.

`cargo run --example text_decoration_example` shows both against a control.

## Frosted glass

The third way to sit a text on a picture is to make the letters a window onto
it, blurred:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
text("09:41")
    .font_size(76.0)
    // The tint over the glass; without one the glyphs are the blur alone.
    .color(Color::rgba(1.0, 1.0, 1.0, 0.35))
    .backdrop_blur(16.0)
# ;
# }
```

This is the same effect a container gets from
[`backdrop_blur`](../concepts/container.md), with the shape of the letters in
place of the shape of the box — CSS's `backdrop-filter` together with
`background-clip: text`. The renderer rasterizes the glyphs into a coverage
mask, blurs the region they cover, and composites the result back through the
mask; the text is then drawn over its own frost, which is why the colour reads
as a tint.

It filters what *this surface* has already drawn — a wallpaper, a photo, the
panel underneath. A container's blur can also reach what the compositor puts
behind the surface, through `ext-background-effect-v1`; that protocol takes a
region, and regions are rectangles, so glyphs cannot be expressed in it.

Three things to know before reaching for it:

- **It is declared on the text.** Every text property is declared on a
  container for the subtree below it; this one cannot, because each frosted
  text ends the render pass to filter the target. It is asked for one text at
  a time on purpose.
- **It is not legibility on its own.** Frost softens the background where a
  stroke and a shadow darken it. A stroke composes with it — over frost it is
  drawn as a true contour, dilated from the same coverage mask and laid outside
  the letter, which is what the glass needs and what a thick stroke wanted
  anyway. A shadow does not: it is still copies of the glyphs *under* the fill,
  so it covers the letter's own area as well as its edge, which is invisible
  under an opaque fill and an opaque letter over frost.
- **A transform carries it along.** The coverage is cut in the text's own
  space and read back through the transform, so a rotated or scaled text keeps
  a frost registered with its letters — which is what an `animate_transform` on
  a frosted label needs, and what it used to lose for the length of the
  animation. A transformed text is rasterized finer for it, so a very large one
  can reach the coverage size ceiling and lose the frost with a warning in the
  log; it is an effect for a label.

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
text("09:41")
    .color(Color::rgba(1.0, 1.0, 1.0, 0.3))
    .backdrop_blur(16.0)
    // A contour, because this text is glass. Elsewhere the same declaration is
    // the cheap approximation, and under an opaque fill the two look alike.
    .text_stroke(TextStroke::new(2.0, Color::BLACK))
# ;
# }
```

`cargo run --example frosted_text` puts all of it beside a control.

## Styling

### Font Size

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
container().child(text("Large text").font_size(24.0));
container().child(text("Small text").font_size(12.0))
# ;
# }
```

### Color

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
container().child(text("Colored text").color(Color::rgb(0.9, 0.3, 0.3)));
container().child(text("White text").color(Color::WHITE))
# ;
# }
```

### Font Family

Set the font family using predefined families or custom font names:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
// Predefined font families
container().child(text("Sans-serif text").font_family(FontFamily::SansSerif));
container().child(text("Serif text").font_family(FontFamily::Serif));
container().child(text("Monospace text").font_family(FontFamily::Monospace));

// Shorthand for monospace
container().child(text("Code example"));

// Custom font by name (if available on system)
container().child(text("Custom font").font_family(FontFamily::name("Inter")))
# ;
# }
```

Available font families:
- `FontFamily::SansSerif` - Default sans-serif font
- `FontFamily::Serif` - Serif font
- `FontFamily::Monospace` - Monospace/fixed-width font
- `FontFamily::Cursive` - Cursive font
- `FontFamily::Fantasy` - Fantasy/decorative font
- `FontFamily::name(&str)` - Custom font by name, interned

### Font Weight

Set the font weight using predefined constants or numeric values (100-900):

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
// Using constants
container().child(text("Thin text").font_weight(FontWeight::THIN));
container().child(text("Light text").font_weight(FontWeight::LIGHT));
container().child(text("Normal text").font_weight(FontWeight::NORMAL));
container().child(text("Medium text").font_weight(FontWeight::MEDIUM));
container().child(text("Semi-bold text").font_weight(FontWeight::SEMI_BOLD));
container().child(text("Bold text").font_weight(FontWeight::BOLD));
container().child(text("Black text").font_weight(FontWeight::BLACK));

// Shorthand for bold
container().child(text("Bold text"));

// Custom numeric weight
container().child(text("Custom weight").font_weight(FontWeight(550)))
# ;
# }
```

Available weight constants:
- `FontWeight::THIN` (100)
- `FontWeight::EXTRA_LIGHT` (200)
- `FontWeight::LIGHT` (300)
- `FontWeight::NORMAL` (400)
- `FontWeight::MEDIUM` (500)
- `FontWeight::SEMI_BOLD` (600)
- `FontWeight::BOLD` (700)
- `FontWeight::EXTRA_BOLD` (800)
- `FontWeight::BLACK` (900)

### Text Wrapping

By default, text wraps to fit the available width. Disable wrapping for single-line text:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
text("This text will not wrap").nowrap()
# ;
# }
```

### Text Alignment

`align` says where each line sits across the text's box. The box is the
text's own: as wide as its widest line, so a wrapped label lines its lines up
against each other, and as wide as the stretch when a layout stretches it.

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
container()
    .width(160.0)
    .child(text("A label long enough to wrap onto a second line").align(TextAlign::Center))
# ;
# }
```

- `TextAlign::Start` (the default) — where the text's direction begins: the
  left for Latin, the right for Arabic or Hebrew
- `TextAlign::Center` — each line centred
- `TextAlign::End` — where the text's direction ends
- `TextAlign::Justified` — every line but a paragraph's last stretched to the
  full width

A text whose box fits its only line looks the same whichever alignment it has:
there is no room for the line to move into. To centre a short label in a wider
space, centre the text in its container, or stretch it across the container
and centre its line.

A line wider than its box — an unwrapped text in a narrow container — starts
at the start and runs off the end, whatever its alignment, as it does in CSS.

`align` takes a signal like the other text properties. A change moves the
glyphs inside the box without resizing it, so it repaints the text and
re-measures nothing.

### Limiting Lines

A text that has to fit a fixed box — a window title in a bar, a track name in
a player — can be held to a number of lines, with the cut marked:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
# let title = "a window title far too long for the bar";
text(title).max_lines(1).overflow(TextOverflow::Ellipsis)
# ;
# }
```

`max_lines` says how many lines the text may take, and the text is measured as
those lines, not as everything it holds. `overflow` says what marks the cut:

- `TextOverflow::Clip` (the default) — the lines past the limit are not drawn
- `TextOverflow::Ellipsis` — the last line drawn ends in `…`
- `TextOverflow::EllipsisStart` — the `…` opens the last line, which keeps the
  end of the text: a path whose file name is what matters
- `TextOverflow::EllipsisMiddle` — the `…` stands in the middle, keeping both
  ends

A line break counts toward the limit like a wrapped line does. A text that fits
is drawn exactly as it would be with no limit.

An unwrapped text is cut by its box already, so an ellipsis on a `nowrap()`
text needs no limit — each line that runs past the box ends in `…`:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
container()
    .width(120.0)
    .child(text("a long track name").nowrap().overflow(TextOverflow::Ellipsis))
# ;
# }
```

Both are reactive like every other text property: `max_lines` takes an
`Option<u32>`, so a signal can lift the limit with `None`.

## Reactive Text

Text content can update based on signals:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
let message = create_signal("Hello".to_string());

text(move || message.get())
# ;
# }
```

### Formatted Reactive Text

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
let count = create_signal(0);

text(move || format!("Count: {}", count.get()))
# ;
# }
```

## Combining Styles

Chain style methods:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
container().child(text("Styled Text").font_family(FontFamily::Serif).font_size(18.0).color(Color::WHITE).nowrap())
# ;
# }
```

## Text in Containers

Text is typically placed inside containers for padding and backgrounds:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
container()
    .padding(12.0)
    .background(Color::rgb(0.2, 0.2, 0.3))
    .corners(4.0)
    .child(
        container().child(text("Button Label").font_size(14.0).color(Color::WHITE))
    )
# ;
# }
```

## Typography Patterns

### Headings

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
container().child(text("Page Title").font_size(24.0).color(Color::WHITE))
# ;
# }
```

### Body Text

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
container().child(text("Regular content text").font_size(14.0).color(Color::rgb(0.8, 0.8, 0.85)))
# ;
# }
```

### Secondary Text

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
container().child(text("Subtitle or caption").font_size(12.0).color(Color::rgb(0.6, 0.6, 0.65)))
# ;
# }
```

### Code/Monospace Text

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
container().child(text("let x = 42;").font_size(13.0).color(Color::rgb(0.6, 0.9, 0.6)))
# ;
# }
```

### Labels

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
container().child(text("LABEL").font_size(11.0).color(Color::rgb(0.5, 0.5, 0.55)))
# ;
# }
```

## App-Level Default Font

Set a default font family for the entire application:

```rust,no_run
# extern crate guido;
# use guido::prelude::*;
# fn view() -> Container { container() }
# fn main() {
# let config = SurfaceConfig::new();
App::new()
    .default_font_family(FontFamily::name("Inter"))
    .run(|app| {
        app.add_surface(config, || view());
    });
# ;
# }
```

All text widgets will use this font family unless they explicitly override it.

## Complete Example

```rust
# extern crate guido;
# use guido::prelude::*;
fn article_card(title: &str, author: &str, preview: &str) -> Container {
    container()
        .padding(16.0)
        .background(Color::rgb(0.12, 0.12, 0.16))
        .corners(8.0)
        .layout(Flex::column().spacing(8.0))
        .child(
            // Title - bold serif
            container().child(text(title).font_family(FontFamily::Serif).font_size(18.0).color(Color::WHITE))
        )
        .child(
            // Author - light weight
            container().child(text(format!("By {}", author)).font_weight(FontWeight::LIGHT).font_size(12.0).color(Color::rgb(0.5, 0.5, 0.6)))
        )
        .child(
            // Preview text
            container().child(text(preview).font_size(14.0).color(Color::rgb(0.7, 0.7, 0.75)))
        )
}
# fn main() {}
```

## API Reference

All properties accept static values, signals, or closures.

```rust,ignore
# // not compiled: a signature listing — these declarations have no bodies.
text(content: impl IntoSignal<String, M>) -> Text

impl Text {
    pub fn font_size<M>(self, size: impl IntoSignal<f32, M>) -> Self;  // numbers work: .font_size(16) or .font_size(16.5)
    pub fn color<M>(self, color: impl IntoSignal<Color, M>) -> Self;
    pub fn font_family<M>(self, family: impl IntoSignal<FontFamily, M>) -> Self;
    pub fn font_weight<M>(self, weight: impl IntoSignal<FontWeight, M>) -> Self;
    pub fn bold(self) -> Self;      // Shorthand for FontWeight::BOLD
    pub fn mono(self) -> Self;      // Shorthand for FontFamily::Monospace
    pub fn wrap<M>(self, wrap: impl IntoSignal<bool, M>) -> Self;
    pub fn nowrap(self) -> Self;    // Shorthand for wrap(false)
    pub fn align<M>(self, align: impl IntoSignal<TextAlign, M>) -> Self;
    pub fn max_lines<M>(self, lines: impl IntoSignal<Option<u32>, M>) -> Self;  // .max_lines(2)
    pub fn overflow<M>(self, overflow: impl IntoSignal<TextOverflow, M>) -> Self;
}
```