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
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
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
# Wayland Layer Shell

Guido uses the Wayland layer shell protocol for positioning widgets on the desktop. This enables status bars, panels, overlays, and multi-surface applications.

## Surface Configuration

Each surface is configured using `SurfaceConfig`:

```rust,no_run
# extern crate guido;
# use guido::prelude::*;
# fn view() -> Container { container() }
# fn main() {
App::new().run(|app| {
    let _surface_id = app.add_surface(
        SurfaceConfig::new()
            .width(1920)
            .height(32)
            .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT)
            .layer(Layer::Top)
            .keyboard_interactivity(KeyboardInteractivity::OnDemand)
            .namespace("my-status-bar")
            .background_color(Color::rgb(0.1, 0.1, 0.15)),
        || view(),
    );
});
# ;
# }
```

Note: `run()` takes a setup closure where you add surfaces. `add_surface()` returns a `SurfaceId` that can be used to get a `SurfaceHandle` for dynamic property modification.

## Layers

Control where your surface appears in the stacking order:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
SurfaceConfig::new().layer(Layer::Top)
# ;
# }
```

| Layer | Description |
|-------|-------------|
| `Background` | Below all windows |
| `Bottom` | Above background, below windows |
| `Top` | Above windows (default) |
| `Overlay` | Above everything |

### Use Cases

- **Background**: Desktop widgets, wallpaper effects
- **Bottom**: Dock bars (below windows but above background)
- **Top**: Status bars, panels (above windows)
- **Overlay**: Notifications, lock screens

## Keyboard Interactivity

Control how the surface receives keyboard focus:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
SurfaceConfig::new().keyboard_interactivity(KeyboardInteractivity::OnDemand)
# ;
# }
```

| Mode | Description |
|------|-------------|
| `None` | Surface never receives keyboard focus |
| `OnDemand` | Surface receives focus when clicked (default) |
| `Exclusive` | Surface grabs keyboard focus exclusively |

### Use Cases

- **None**: Status bars that only respond to mouse
- **OnDemand**: Panels with text input fields
- **Exclusive**: Lock screens, app launchers, modal dialogs

## Anchoring

Control which screen edges the surface attaches to:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
SurfaceConfig::new().anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT)
# ;
# }
```

| Anchor | Effect |
|--------|--------|
| `TOP` | Attach to top edge |
| `BOTTOM` | Attach to bottom edge |
| `LEFT` | Attach to left edge |
| `RIGHT` | Attach to right edge |

### Common Patterns

**Top status bar (full width):**
```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
SurfaceConfig::new()
    .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT)
    .height(32)
# ;
# }
```

**Bottom dock (full width):**
```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
SurfaceConfig::new()
    .anchor(Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT)
    .height(48)
# ;
# }
```

**Left sidebar (full height):**
```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
SurfaceConfig::new()
    .anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT)
    .width(64)
# ;
# }
```

**Corner widget (top-right):**
```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
SurfaceConfig::new()
    .anchor(Anchor::TOP | Anchor::RIGHT)
    .width(200)
    .height(100)
# ;
# }
```

**Centered floating (no anchors):**
```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
// No anchor = centered on screen
SurfaceConfig::new()
    .width(400)
    .height(300)
# ;
# }
```

## Size Behavior

Size depends on anchoring:

- **Anchored dimension**: Expands to fill (e.g., width when LEFT+RIGHT anchored)
- **Unanchored dimension**: Uses specified size
- **No anchors**: Uses exact size, centered on screen

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
// Width fills screen, height is 32px
SurfaceConfig::new()
    .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT)
    .height(32);

// Both dimensions specified, widget is 200x100
SurfaceConfig::new()
    .anchor(Anchor::TOP | Anchor::RIGHT)
    .width(200)
    .height(100)
# ;
# }
```

## Namespace

Identify your surface to the compositor:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
SurfaceConfig::new().namespace("my-app-name")
# ;
# }
```

Some compositors use this for:
- Workspace rules
- Blur effects
- Per-app settings

## Exclusive Zones

Reserve screen space (windows won't overlap):

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
SurfaceConfig::new()
    .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT)
    .height(32)
    .exclusive_zone(32)  // Reserve 32px at top
# ;
# }
```

Without exclusive zone, windows can cover the surface.

The compositor adds a margin on the anchored edge on top of the zone (wlroots
compositors such as sway, and smithay ones such as niri, both do), so
`.margin([8, 0, 0, 0])` with `.exclusive_zone(32)` keeps windows 40px from the
top. `ExclusiveZone::Auto` reserves the surface's own size plus the margin facing
away from the edge: a gap below a top bar is `.margin([0, 0, 8, 0])`.

## Multi-Surface Applications

Guido supports creating multiple surfaces within a single application. All surfaces share the same reactive state, allowing for coordinated updates.

### Multiple Static Surfaces

Define multiple surfaces at startup:

```rust,no_run
# extern crate guido;
# use guido::prelude::*;
fn main() {
    App::new().run(|app| {
        // Shared reactive state
        let count = create_signal(0);

        // Top status bar
        app.add_surface(
            SurfaceConfig::new()
                .height(32)
                .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT)
                .layer(Layer::Top)
                .namespace("status-bar")
                .background_color(Color::rgb(0.1, 0.1, 0.15)),
            move || {
                container()
                    .height(fill())
                    .layout(
                        Flex::row()
                            .main_alignment(MainAlignment::SpaceBetween)
                            .cross_alignment(CrossAlignment::Center)
                    )
                    .padding([0.0, 16.0])
                    .child(text("Status Bar"))
                    .child(text(move || format!("Count: {}", count.get())))
            },
        );

        // Bottom dock
        app.add_surface(
            SurfaceConfig::new()
                .height(48)
                .anchor(Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT)
                .layer(Layer::Top)
                .namespace("dock")
                .background_color(Color::rgb(0.15, 0.15, 0.2)),
            move || {
                container()
                    .height(fill())
                    .layout(Flex::row().spacing(16.0).center())
                    .child(
                        container()
                            .padding([8.0, 16.0])
                            .background(Color::rgb(0.3, 0.3, 0.4))
                            .corners(8.0)
                            .when_hovered(|s| s.lighter(0.1))
                            .on_click(move || count.update(|c| *c += 1))
                            .child(container().child(text("+").color(Color::WHITE)))
                    )
            },
        );
    });
}
```

### Key Points

- **Shared State**: All surfaces share the same reactive signals
- **Independent Widget Trees**: Each surface has its own widget tree
- **Fill Layout**: Use `height(fill())` to make containers expand to fill the surface

### Dynamic Surfaces

Create and destroy surfaces at runtime using `spawn_surface()`:

```rust,no_run
# extern crate guido;
# use guido::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    App::new().run(|app| {
        let popup_handle: Rc<RefCell<Option<SurfaceHandle>>> = Rc::new(RefCell::new(None));
        let popup_clone = popup_handle.clone();

        app.add_surface(
            SurfaceConfig::new()
                .height(32)
                .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT),
            move || {
                container()
                    .child(
                        container()
                            .padding(8.0)
                            .when_hovered(|s| s.lighter(0.1))
                            .on_click({
                                let popup_handle = popup_clone.clone();
                                move || {
                                    let mut handle = popup_handle.borrow_mut();
                                    if let Some(h) = handle.take() {
                                        // Close existing popup
                                        h.close();
                                    } else {
                                        // Create new popup
                                        let new_handle = spawn_surface(
                                            SurfaceConfig::new()
                                                .width(200)
                                                .height(300)
                                                .anchor(Anchor::TOP | Anchor::RIGHT)
                                                .layer(Layer::Overlay)
                                                .keyboard_interactivity(KeyboardInteractivity::Exclusive),
                                            || {
                                                container()
                                                    .padding(16.0)
                                                    .child(text("Popup Content"))
                                            }
                                        );
                                        *handle = Some(new_handle);
                                    }
                                }
                            })
                            .child(text("Toggle Popup"))
                    )
            },
        );
    });
}
```

### SurfaceHandle API

The `SurfaceHandle` allows controlling a surface after creation:

```rust,ignore
# // not compiled: a signature listing — these declarations have no bodies.
impl SurfaceHandle {
    /// Close and destroy the surface. Input already queued for it is
    /// not delivered once this is called
    pub fn close(&self);

    /// Get the surface ID
    pub fn id(&self) -> SurfaceId;

    /// Change the layer (Background, Bottom, Top, Overlay)
    pub fn set_layer(&self, layer: Layer);

    /// Change keyboard interactivity mode
    pub fn set_keyboard_interactivity(&self, mode: KeyboardInteractivity);

    /// Change anchor edges
    pub fn set_anchor(&self, anchor: Anchor);

    /// Change surface size — same vocabulary as `SurfaceConfig::width`,
    /// so an axis can be handed back to `content()`
    pub fn set_size(&self, width: impl Into<SurfaceExtent>, height: impl Into<SurfaceExtent>);

    /// Change the screen-space reservation policy
    pub fn set_exclusive_zone(&self, zone: impl Into<ExclusiveZone>);

    /// Change margins — CSS shorthands, as `padding` takes
    pub fn set_margin(&self, margin: impl Into<Margin>);
}
```

### Getting a Handle for Existing Surfaces

Use `surface_handle()` to get a handle for any surface by its ID:

```rust,no_run
# extern crate guido;
# use guido::prelude::*;
# fn main() {
# let config = SurfaceConfig::new();
App::new().run(|app| {
    // Store the ID when adding the surface
    let status_bar_id = app.add_surface(config, move || {
        container().child(text("Status bar"))
    });

    // Get a handle and modify properties dynamically, from anywhere
    let handle = surface_handle(status_bar_id);
    handle.set_layer(Layer::Overlay);
    handle.set_keyboard_interactivity(KeyboardInteractivity::Exclusive);
});
# ;
# }
```

## Multiple Outputs (Monitors)

Connected outputs are exposed as a reactive list. Reading it inside a
tracked closure (an effect, a dynamic child, a reactive property)
re-runs the closure when monitors are plugged in, unplugged, or
reconfigured:

```rust
# extern crate guido;
# fn main() {
use guido::prelude::*;

create_effect(move || {
    for info in outputs().get() {
        println!(
            "{:?}: {:?} {} ({}x{:?})",
            info.id, info.name, info.model, info.scale_factor, info.logical_size
        );
    }
});
# ;
# }
```

Each `OutputInfo` carries a stable `OutputId` plus the connector name
(`"DP-1"`, `"eDP-1"`, …), description, make/model, integer scale factor,
and logical size/position when the compositor reports them. Ids are never
reused: a monitor that is unplugged and reconnected gets a fresh id.

### Display Scale

Scaling is automatic and there is nothing to configure. Widgets are laid
out in logical pixels and the surface is drawn at whatever scale its
output is set to — 1.5 and 1.25 included, where the compositor offers
`wp_fractional_scale_v1` and `wp_viewporter`. Where it does not, the
surface falls back to the whole-number scale the compositor asks for.

`OutputInfo::scale_factor` is not that number. It is the integer
`wl_output` scale, which is what a compositor reports for a monitor and
is rounded up on a fractionally-scaled one — a 1.5 output reports 2. It
is there to describe the monitor, not to size anything.

### Pinning a Surface to an Output

By default the compositor picks the output a surface appears on. Pass an
`OutputId` to pin it:

```rust,no_run
# extern crate guido;
# use guido::prelude::*;
# fn bar_widget() -> Container { container() }
# fn main() {
# for info in outputs().get() {
spawn_surface(
    SurfaceConfig::new()
        .height(32)
        .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT)
        .output(info.id),
    move || bar_widget(),
);
# }
# }
```

The output cannot be changed after creation — a layer surface is bound to
its output for its lifetime. If the output disconnects before the surface
is created, the compositor chooses one instead (a warning is logged).

### One Bar per Monitor

An app can start with **zero surfaces** and spawn one per output from an
effect — the classic multi-monitor status bar. See
`examples/multi_output.rs` for the full version:

```rust,no_run
# extern crate guido;
# use guido::prelude::*;
# use std::cell::RefCell;
# use std::collections::HashMap;
# use std::rc::Rc;
# fn bar_widget() -> Container { container() }
# fn main() {
App::new().run(|_app| {
    let bars: Rc<RefCell<HashMap<OutputId, SurfaceHandle>>> =
        Rc::new(RefCell::new(HashMap::new()));

    create_effect(move || {
        let current = outputs().get();
        let mut bars = bars.borrow_mut();

        // Drop bars for disconnected outputs
        bars.retain(|id, handle| {
            let alive = current.iter().any(|o| o.id == *id);
            if !alive {
                handle.close();
            }
            alive
        });

        // Spawn a bar on every new output
        for info in current {
            if !bars.contains_key(&info.id) {
                let handle = spawn_surface(
                    SurfaceConfig::new().height(32).output(info.id),
                    move || bar_widget(),
                );
                bars.insert(info.id, handle);
            }
        }
    });
});
# ;
# }
```

When the compositor closes the surfaces of an unplugged monitor, the
bars keep working on the remaining outputs. When the last monitor goes,
its bar is the last surface, and closing the last surface ends the app —
unless it was built with `App::quit_on_last_surface(false)`, in which case
it idles until a monitor comes back and the effect spawns a bar for it.

### Tracking Which Output a Surface Is On

`surface_output(surface_id)` reports the output a surface is currently
shown on (`None` until the compositor maps it). It is a tracked read —
reactive inside any tracked closure:

```rust,no_run
# extern crate guido;
# use guido::prelude::*;
# fn main() {
# let my_surface_id = SurfaceId::next();
text(move || match surface_output(my_surface_id) {
    Some(out) => format!("shown on output {}", out.raw()),
    None => "not mapped yet".to_string(),
})
# ;
# }
```

For a surface spanning multiple outputs, this reports the one entered
most recently.

## Input Regions

By default the whole surface accepts pointer and touch input. A container can
say otherwise, and what it says goes to the compositor: everything outside the
region lets clicks reach the windows below.

The surface declares the baseline and a container declares the exception. An
overlay that lets everything past, with one pill that does not:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
SurfaceConfig::new()
    .background_color(Color::TRANSPARENT)
    .click_through();

container()
    .takes_input(true)
    .corners(20.0)
    .on_click(|| {})
# ;
# }
```

And the other way round, a bar that takes input with a notch the desktop gets:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
container().takes_input(false).width(200.0)
# ;
# }
```

The region is read off the frame that was drawn, so it follows the shape on
screen: its corners, its transform, its clip. A container that stops painting —
hidden, culled, scrolled out of view — takes its region with it, and a frame
that changes nothing publishes nothing. `examples/input_region.rs` is the first
of the two, in full.

The transform half of that is newer than the sentence. A turned container's
region was the axis-aligned box around it, which claims more than it drew: at
45° an 80×80 container takes clicks over 56% more area than it covers, and every
one of those clicks lands on a panel that is not there rather than reaching the
window below. The region is cut from the placed shape now, and so is whatever
clips it — so a card filling a rounded scroller takes clicks in the scroller's
rounded shape rather than in the square around it.

**What a container declares is its own shape.** Children are covered because
they are drawn inside it, not because the declaration reaches them. A child
drawn *outside* its parent — `overflow` is `Visible` by default, so a transform
or a size larger than the parent does it — is outside the region as well, and on
a click-through surface a handler on that child never runs:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
# let do_thing = || {};
container()
    .takes_input(true)
    .width(100.0)
    .height(30.0)
    // 40px below the parent, so outside the region: those clicks go to the
    // desktop, not to this handler.
    .child(container().translate((0.0, 40.0)).on_click(do_thing))
# ;
# }
```

Declare `takes_input` on the container whose shape you mean, and a nested
declaration of the opposite kind is simply a smaller area declared later — an
island inside a hole works with no rule of its own.

`takes_input(false)` is not the same as
[`enabled(false)`](../interactivity/disabled.md): a disabled container
refuses input and still blocks what is behind the surface, which is what a
disabled button should do.

For a region that belongs to no widget, the surface still takes rectangles
directly:

```rust,no_run
# extern crate guido;
# use guido::prelude::*;
# fn main() {
# let id = SurfaceId::next();
# let rect = Rect::new(16.0, 20.0, 200.0, 40.0);
SurfaceConfig::new().input_region([Rect::new(16.0, 20.0, 200.0, 40.0)]);
surface_handle(id).set_input_region(Some(vec![rect]));
surface_handle(id).set_input_region(None);
# ;
# }
```

Note: keyboard focus is unaffected — use `keyboard_interactivity` for
that. Rectangles are rounded outward to whole pixels.

## Backdrop Blur

`backdrop_blur` blurs whatever is behind a container. There are two
things behind it, on opposite sides of the Wayland surface, and both
are filtered:

- what **this surface** already drew — a wallpaper, a photo, the panel
  under a card;
- what the **compositor** composites below the surface, through
  `ext-background-effect-v1`, wherever the surface is translucent.

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
container()
    .background(Color::rgba(0.12, 0.12, 0.18, 0.55))
    .corners(16.0)
    .backdrop_blur(32.0)
    .child(text("Frosted glass"))
# ;
# }
```

Filtering both is not a compromise between mechanisms. Blur is a linear
operator, so over a region of uniform alpha `a`:

```text
blur(a·ours + (1−a)·theirs) = a·blur(ours) + (1−a)·blur(theirs)
```

Blurring each layer separately and compositing gives exactly the same
result — which is why neither is chosen over the other. A translucent
panel is neither "ours" nor "theirs" at any pixel, so a per-box choice
could only be right for some of them.

Restrict it when only one side should soften:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
// Desktop behind the panel softens; the panel's own islands stay crisp.
container().backdrop_blur(
    BackdropBlur::new(24.0).sources(BackdropSources::COMPOSITOR),
)
# ;
# }
```

### What each side costs

The **compositor** side is a region handed over once per change: the
region follows the container automatically as layout moves, resizes and
(animated) corner radii change. Rounded corners are tessellated into
small rectangles because `wl_region` has no notion of curves, and the
protocol carries no radius — the compositor picks its own, so `radius`
does not apply there. Check availability with `compositor_effects()`.

Which is why a blur restricted to the compositor asks for its region on
its sources alone, and a radius of zero there is a request rather than a
refusal:

```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
// Blur nothing of our own; let the compositor blur the desktop behind us.
container().backdrop_blur(
    BackdropBlur::new(0.0).sources(BackdropSources::COMPOSITOR),
)
# ;
# }
```

Everywhere the surface is a source, a radius of zero still means no blur
— that is how a blur is switched off by the same signal that switches it
on. To switch off a compositor-only one, empty its sources instead.

The **surface** side is guido's own: the frame is drawn into an
offscreen target, each region is downsampled, blurred with a separable
gaussian and composited back masked to the container's rounded shape.
Frames with no backdrop blur skip all of it and draw straight to the
swapchain. A text can ask for the same thing with the shape of its
glyphs as the mask — see [Frosted glass](../building-ui/text.md#frosted-glass)
— and that one is surface-side only, since a `wl_region` cannot hold a
letter.

Two skips keep that honest, and both are unobservable — they drop work
that could not have changed a pixel, so they may switch on and off
between frames without anything showing:

- a region with nothing drawn beneath it is not filtered at all;
- surfaces that never asked for a compositor region are left untouched,
  so compositor-side rules (e.g. blur by namespace) keep working.

### The seam

Where alpha *varies* within the blur radius — at the edges of opaque
content inside the box — the two blurs do not bleed into each other:
the desktop does not smear into the surface's own content or the other
way round. That cannot be fixed from guido. `set_blur_region` is
fire-and-forget: it takes a region and returns nothing, so the
compositor's pixels are never ours to filter across.

See `examples/blur_example.rs` and `examples/draw_order_example.rs`.

## Popups (Menus, Dropdowns)

`spawn_popup` creates an **xdg popup** anchored to a surface: the
compositor positions it relative to an anchor rectangle, keeps it on
screen (flipping/sliding at screen edges), and — with `.grab()` —
dismisses it when the user clicks outside. Real menu semantics, no
fullscreen overlay:

```rust,no_run
# extern crate guido;
# use guido::prelude::*;
# fn menu_widget() -> Container { container() }
# fn main() {
# let bar_id = SurfaceId::next();
# let button_ref = create_widget_ref();
let popup = spawn_popup(
    bar_id,
    PopupConfig::new(250)                      // width; height sizes to content
        .anchor_rect(button_ref.rect().get())  // parent surface coords
        .anchor(PopupAnchor::Bottom)           // attach point on the rect
        .gravity(PopupGravity::Bottom)         // growth direction
        .grab(),                               // dismiss on outside click
    move || menu_widget(),
);
# ;
# }
```

Dismissal is reactive — reset your open/closed state when the
compositor closes the popup:

```rust,no_run
# extern crate guido;
# use guido::prelude::*;
# fn main() {
# let menu_open = create_signal(false);
# let popup = spawn_popup(SurfaceId::next(), PopupConfig::new(250), container);
create_effect(move || {
    if popup.dismissed() {
        menu_open.set(false);
    }
});
# ;
# }
```

`popup.close()` closes it programmatically, and from that call on
neither it nor a popup hanging from it is handed any more input — a
second click already queued on the item that closed the menu picks
nothing. Popups render their own
widget tree and share the app's reactive state like any surface;
anchoring a popup to another popup creates a nested popup (submenus).
A handle outlives its popup: reading `dismissed()` is safe after the
popup is gone, including from a submenu whose parent closed with it.

Note: for a bottom bar use `anchor(Top)` + `gravity(Top)` so the menu
opens upward — or just rely on the compositor's flip adjustment.

See `examples/popup_example.rs`.

## Session Lock (Lock Screens)

`lock_session` asks the compositor to lock the session
(`ext-session-lock-v1`) and, without waiting for the answer, creates
one lock surface per output using your widget factory, as the protocol
asks. Once the compositor grants the lock it blanks every output, shows
the lock surfaces, and routes all input to them, so a `password_input`
works out of the box:

```rust,no_run
# extern crate guido;
# use guido::prelude::*;
# fn verify_password(_attempt: &str) -> bool { true }
# fn main() {
lock_session(|output: OutputInfo| {
    let attempt = create_password();
    container()
        .width(fill())
        .height(fill())
        .background(Color::rgb(0.07, 0.07, 0.1))
        .layout(Flex::column().main_alignment(MainAlignment::Center))
        .child(text(format!("Locked — {:?}", output.name)))
        .child(password_input(attempt).on_submit(move || {
            if verify_password(attempt.take().expose()) {
                unlock_session();
            }
        }))
});
# ;
# }
```

The lifecycle is reactive:

```text
text(move || format!("{:?}", lock_state().get()))
// Unlocked → Locking → Locked, back to Unlocked on unlock/denial
```

Details worth knowing:

- Outputs plugged in **while locking or locked** get a lock surface
  automatically (the factory is called again); the compositor blanks
  any output without one.
- If the compositor refuses the lock (no protocol support, or another
  lock client is active), `lock_state` returns to `Unlocked` and the
  lock surfaces already made are dropped.
- Unlike ordinary surfaces, closing lock surfaces never exits the app:
  a lock daemon whose only surfaces are lock surfaces keeps running
  after unlocking, idle until the next `lock_session` call.
- Layer-shell properties (anchor, margins, exclusive zone…) don't
  apply to lock surfaces; their size always comes from the compositor.

See `examples/simple_lock.rs` — note that running it really locks your
session (it has a 30-second auto-unlock safety net; the password is
`guido`).

## Complete Examples

### Status Bar

```rust,no_run
# extern crate guido;
# use guido::prelude::*;
# fn center_section() -> Container { container() }
# fn left_section() -> Container { container() }
# fn right_section() -> Container { container() }
fn main() {
    App::new().run(|app| {
        app.add_surface(
            SurfaceConfig::new()
                .height(32)
                .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT)
                .layer(Layer::Top)
                .exclusive_zone(32u32)
                .namespace("status-bar")
                .background_color(Color::rgb(0.1, 0.1, 0.15)),
            || {
                container()
                    .height(fill())
                    .layout(
                        Flex::row()
                            .main_alignment(MainAlignment::SpaceBetween)
                            .cross_alignment(CrossAlignment::Center)
                    )
                    .children([
                        left_section(),
                        center_section(),
                        right_section(),
                    ])
            },
        );
    });
}
```

### Dock

```rust,no_run
# extern crate guido;
# use guido::prelude::*;
# fn dock_icon(_name: &str) -> Container { container() }
fn main() {
    App::new().run(|app| {
        app.add_surface(
            SurfaceConfig::new()
                .height(64)
                .anchor(Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT)
                .layer(Layer::Top)
                .exclusive_zone(64u32)
                .namespace("dock")
                .background_color(Color::rgba(0.1, 0.1, 0.15, 0.9)),
            || {
                container()
                    .height(fill())
                    .layout(Flex::row().spacing(8.0).center())
                    .children([
                        dock_icon("terminal"),
                        dock_icon("browser"),
                        dock_icon("files"),
                    ])
            },
        );
    });
}
```

### Floating Overlay with Keyboard Focus

```rust,no_run
# extern crate guido;
# use guido::prelude::*;
fn main() {
    App::new().run(|app| {
        app.add_surface(
            SurfaceConfig::new()
                .width(300)
                .height(100)
                .anchor(Anchor::TOP | Anchor::RIGHT)
                .layer(Layer::Overlay)
                .keyboard_interactivity(KeyboardInteractivity::Exclusive)
                .namespace("notification")
                .background_color(Color::TRANSPARENT),
            || {
                container()
                    .padding(20.0)
                    .background(Color::rgb(0.15, 0.15, 0.2))
                    .corners(12.0)
                    .child(container().child(text("Notification").color(Color::WHITE)))
            },
        );
    });
}
```

## API Reference

### SurfaceConfig

```rust,ignore
# // not compiled: a signature listing — these declarations have no bodies.
impl SurfaceConfig {
    pub fn new() -> Self;
    pub fn width(self, width: impl Into<SurfaceExtent>) -> Self;
    pub fn height(self, height: impl Into<SurfaceExtent>) -> Self;
    pub fn anchor(self, anchor: Anchor) -> Self;
    pub fn layer(self, layer: Layer) -> Self;
    pub fn keyboard_interactivity(self, mode: KeyboardInteractivity) -> Self;
    pub fn exclusive_zone(self, zone: impl Into<ExclusiveZone>) -> Self;
    pub fn namespace(self, namespace: impl Into<String>) -> Self;
    pub fn background_color(self, color: Color) -> Self;
    pub fn margin(self, margin: impl Into<Margin>) -> Self;
    pub fn output(self, output: OutputId) -> Self;
    pub fn input_region(self, rects: impl Into<Vec<Rect>>) -> Self;
    pub fn click_through(self) -> Self;
}
```

### Outputs

```rust,ignore
# // not compiled: a signature listing — these declarations have no bodies.
/// Reactive list of connected outputs, sorted by id
pub fn outputs() -> Signal<Vec<OutputInfo>>;

/// The output a surface is currently shown on (tracked read)
pub fn surface_output(id: SurfaceId) -> Option<OutputId>;

pub struct OutputInfo {
    pub id: OutputId,
    pub name: Option<String>,        // connector, e.g. "DP-1"
    pub description: Option<String>,
    pub make: String,
    pub model: String,
    pub scale_factor: i32,
    pub logical_size: Option<(i32, i32)>,
    pub logical_position: Option<(i32, i32)>,
}
```

### App

```rust,ignore
# // not compiled: a signature listing — these declarations have no bodies.
impl App {
    pub fn new() -> Self;
    pub fn run(self, setup: impl FnOnce(&mut Self)) -> ExitReason;
    pub fn add_surface<W, F>(&mut self, config: SurfaceConfig, widget_fn: F) -> SurfaceId
    where
        W: Widget + 'static,
        F: FnOnce() -> W + 'static;
}
```

### Dynamic Surface Creation

```rust,ignore
# // not compiled: a signature listing — these declarations have no bodies.
/// Spawn a new surface at runtime
pub fn spawn_surface<W, F>(config: SurfaceConfig, widget_fn: F) -> SurfaceHandle
where
    W: Widget + 'static,
    F: FnOnce() -> W + Send + 'static;

/// Get a handle for an existing surface by ID
pub fn surface_handle(id: SurfaceId) -> SurfaceHandle;
```

### SurfaceHandle

```rust,ignore
# // not compiled: a signature listing — these declarations have no bodies.
impl SurfaceHandle {
    pub fn id(&self) -> SurfaceId;
    pub fn close(&self);
    pub fn set_layer(&self, layer: Layer);
    pub fn set_keyboard_interactivity(&self, mode: KeyboardInteractivity);
    pub fn set_anchor(&self, anchor: Anchor);
    pub fn set_size(&self, width: impl Into<SurfaceExtent>, height: impl Into<SurfaceExtent>);
    pub fn set_exclusive_zone(&self, zone: impl Into<ExclusiveZone>);
    pub fn set_margin(&self, margin: impl Into<Margin>);
    pub fn set_input_region(&self, rects: Option<Vec<Rect>>);
}
```