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
//! xdg popups anchored to a bar: real menu semantics.
//!
//! Clicking the button opens a grabbed popup under it — the compositor
//! positions it (flipping/sliding at screen edges) and dismisses it when
//! you click anywhere outside. No fullscreen overlay involved.
//!
//! "Settings ▸" opens a **nested** popup, parented to the first one rather than
//! to the bar. That is the case with the sharpest failure: a popup must be
//! destroyed before its parent or the compositor raises `not_the_topmost_popup`
//! and closes the connection, so getting the teardown order wrong does not draw
//! something odd, it kills the application. Worth reaching by hand — dismiss the
//! pair with one outside click, and close the parent while the child is open.
use std::cell::RefCell;
use std::rc::Rc;
use guido::prelude::*;
fn menu_entry(label: &str) -> Container {
container()
.padding([8.0, 12.0])
.corners(6.0)
.when_hovered(|s| s.lighter(0.12))
.child(text(label).font_size(13))
}
/// The entry that opens a popup parented to the popup it lives in.
///
/// The parent arrives through a slot rather than by value: the factory that
/// builds a popup's content runs *before* `spawn_popup` hands back its handle,
/// so at the time this entry is built its own parent does not exist yet.
fn submenu_entry(parent: Rc<RefCell<Option<PopupHandle>>>, open: RwSignal<bool>) -> Container {
let entry_ref = create_widget_ref();
let child_slot: Rc<RefCell<Option<PopupHandle>>> = Rc::new(RefCell::new(None));
container()
.widget_ref(entry_ref)
.padding([8.0, 12.0])
.corners(6.0)
.when_hovered(|s| s.lighter(0.12))
.on_click(move || {
if open.get() {
if let Some(child) = child_slot.borrow_mut().take() {
child.close();
}
return;
}
let Some(parent_id) = parent.borrow().as_ref().map(|p| p.id()) else {
return;
};
// Parented to the popup, not to the bar: that is what makes it a
// nested one, and what puts it above its parent in the grab chain.
let child = spawn_popup(
parent_id,
PopupConfig::new(180)
.anchor_rect(entry_ref.rect().get())
.anchor(PopupAnchor::Right)
.gravity(PopupGravity::Right)
.grab()
.background_color(Color::TRANSPARENT),
|| {
container()
.width(fill())
.background(Color::rgb(0.16, 0.16, 0.24))
.corners(10.0)
.padding(8.0)
.layout(Flex::column().spacing(2.0))
.child(menu_entry("Appearance"))
.child(menu_entry("Keyboard"))
.child(menu_entry("Network"))
},
);
open.set(true);
create_effect(move || {
if child.dismissed() {
open.set(false);
}
});
*child_slot.borrow_mut() = Some(child);
})
.child(
text(move || {
if open.get() {
"Settings ◂".to_string()
} else {
"Settings ▸".to_string()
}
})
.font_size(13),
)
}
fn main() {
env_logger::init();
App::new().run(|app| {
let button_ref = create_widget_ref();
let menu_open = create_signal(false);
let submenu_open = create_signal(false);
let popup_slot: Rc<RefCell<Option<PopupHandle>>> = Rc::new(RefCell::new(None));
// The bar id is only known after add_surface returns; the click
// handler runs later, so a slot is enough.
let bar_id_slot: Rc<RefCell<Option<SurfaceId>>> = Rc::new(RefCell::new(None));
let bar_id_for_click = bar_id_slot.clone();
let bar_id = app.add_surface(
SurfaceConfig::new()
.height(36)
.anchor(Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT)
.background_color(Color::rgb(0.1, 0.1, 0.15)),
move || {
let popup_slot = popup_slot.clone();
// The same slot the parent handle lands in: the submenu reads
// it at click time, by which point it holds its parent.
let popup_slot_for_child = popup_slot.clone();
container()
.width(fill())
.layout(Flex::row().spacing(12.0).center())
.child(
container()
.widget_ref(button_ref)
.padding([6.0, 16.0])
.background(move || {
if menu_open.get() {
Color::rgb(0.35, 0.35, 0.5)
} else {
Color::rgb(0.25, 0.25, 0.35)
}
})
.corners(6.0)
.when_hovered(|s| s.lighter(0.1))
.on_click(move || {
if menu_open.get() {
// Toggle closed
if let Some(popup) = popup_slot.borrow_mut().take() {
popup.close();
}
return;
}
let popup = spawn_popup(
bar_id_for_click.borrow().unwrap(),
PopupConfig::new(220)
.anchor_rect(button_ref.rect().get())
.anchor(PopupAnchor::Top)
.gravity(PopupGravity::Top)
.grab()
.background_color(Color::TRANSPARENT),
{
let submenu_parent = popup_slot_for_child.clone();
move || {
// Auto-height popup: the content
// wraps, no fill-height here
container()
.width(fill())
.background(Color::rgb(0.13, 0.13, 0.2))
.corners(10.0)
.padding(8.0)
.layout(Flex::column().spacing(2.0))
.child(menu_entry("Profile"))
.child(submenu_entry(submenu_parent, submenu_open))
.child(menu_entry("About"))
.child(menu_entry("Quit"))
}
},
);
menu_open.set(true);
// Reset state when the compositor dismisses
// the popup (outside click) or close() runs
create_effect(move || {
if popup.dismissed() {
menu_open.set(false);
}
});
*popup_slot.borrow_mut() = Some(popup);
})
.child(text(move || {
if menu_open.get() {
"Menu ▾ (click outside to dismiss)".to_string()
} else {
"Menu ▸".to_string()
}
})),
)
},
);
*bar_id_slot.borrow_mut() = Some(bar_id);
});
}