1use std::sync::Arc;
4
5use colors::BASE_COLOR_VAR;
6use zng_ext_input::{
7 focus::{FOCUS, FOCUS_CHANGED_EVENT, WidgetInfoFocusExt as _},
8 keyboard::{KEY_INPUT_EVENT, Key, KeyState},
9};
10use zng_layout::unit::Orientation2D;
11use zng_wgt::{base_color, border, prelude::*};
12use zng_wgt_fill::background_color;
13use zng_wgt_input::{
14 gesture::mnemonic_scope,
15 pointer_capture::{CaptureMode, capture_pointer_on_init},
16};
17use zng_wgt_layer::popup::{POPUP, POPUP_CLOSE_CMD, POPUP_CLOSE_REQUESTED_EVENT, PopupCloseMode};
18use zng_wgt_stack::Stack;
19use zng_wgt_style::{impl_style_fn, style_fn};
20
21use super::sub::{HOVER_OPEN_DELAY_VAR, SubMenuWidgetInfoExt};
22
23#[widget($crate::popup::SubMenuPopup)]
25pub struct SubMenuPopup(zng_wgt_layer::popup::Popup);
26impl SubMenuPopup {
27 fn widget_intrinsic(&mut self) {
28 self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
29 widget_set! {
30 self;
31
32 capture_pointer_on_init = CaptureMode::Subtree;
39
40 mnemonic_scope = true;
41 zng_wgt_rule_line::collapse_scope = true;
42 zng_wgt_size_offset::max_height = 90.pct();
43 }
44
45 self.widget_builder().push_build_action(|wgt| {
46 let id = wgt.capture_value::<WidgetId>(property_id!(Self::parent_id));
47 let children = wgt
48 .capture_property(property_id!(Self::children))
49 .map(|p| p.args.ui_node(0).clone())
50 .unwrap_or_else(|| ArcNode::new(ui_vec![]));
51
52 wgt.set_child(sub_menu_popup_node(children, id));
53 });
54 }
55}
56impl_style_fn!(SubMenuPopup, DefaultStyle);
57
58#[property(CHILD, default(ui_vec![]), widget_impl(SubMenuPopup))]
60pub fn children(wgt: &mut WidgetBuilding, children: impl IntoUiNode) {
61 let _ = children;
62 wgt.expect_property_capture();
63}
64
65#[property(CONTEXT, widget_impl(SubMenuPopup))]
67pub fn parent_id(wgt: &mut WidgetBuilding, submenu_id: impl IntoValue<WidgetId>) {
68 let _ = submenu_id;
69 wgt.expect_property_capture();
70}
71
72context_var! {
73 pub static PANEL_FN_VAR: WidgetFn<zng_wgt_panel::PanelArgs> = WidgetFn::new(default_panel_fn);
79}
80
81#[property(CONTEXT, default(PANEL_FN_VAR), widget_impl(SubMenuPopup, DefaultStyle))]
85pub fn panel_fn(child: impl IntoUiNode, panel: impl IntoVar<WidgetFn<zng_wgt_panel::PanelArgs>>) -> UiNode {
86 with_context_var(child, PANEL_FN_VAR, panel)
87}
88
89#[widget($crate::popup::DefaultStyle)]
91pub struct DefaultStyle(zng_wgt_layer::popup::DefaultStyle);
92impl DefaultStyle {
93 fn widget_intrinsic(&mut self) {
94 widget_set! {
95 self;
96
97 super::sub::style_fn = style_fn!(|_| super::sub::SubMenuStyle!());
98
99 base_color = light_dark(rgb(0.82, 0.82, 0.82), rgb(0.18, 0.18, 0.18));
100 background_color = BASE_COLOR_VAR.rgba();
101 border = {
102 widths: 1,
103 sides: BASE_COLOR_VAR.shade_into(1),
104 };
105 }
106 crate::MENU_TEXT_INPUT.set_label_style(self);
107 }
108}
109
110pub fn default_panel_fn(args: zng_wgt_panel::PanelArgs) -> UiNode {
114 let scroll_id = WidgetId::new_unique();
116 zng_wgt_scroll::cmd::SCROLL_UP_CMD
117 .scoped(scroll_id)
118 .shortcut()
119 .set(Shortcuts::new());
120 zng_wgt_scroll::cmd::SCROLL_DOWN_CMD
121 .scoped(scroll_id)
122 .shortcut()
123 .set(Shortcuts::new());
124
125 zng_wgt_scroll::Scroll! {
126 id = scroll_id;
127 focusable = false;
128 child_align = Align::FILL;
129 child = Stack! {
130 children_align = Align::FILL;
131 children = args.children;
132 direction = zng_wgt_stack::StackDirection::top_to_bottom();
133 };
134 mode = zng_wgt_scroll::ScrollMode::VERTICAL;
135 }
136}
137
138pub fn sub_menu_popup_node(children: ArcNode, parent: Option<WidgetId>) -> UiNode {
140 let child = zng_wgt_panel::node(
141 children,
142 if parent.is_none() {
143 super::context::PANEL_FN_VAR
144 } else {
145 PANEL_FN_VAR
146 },
147 );
148 let mut close_timer = None::<DeadlineVar>;
149 match_node(child, move |c, op| match op {
150 UiNodeOp::Init => {
151 let id = WIDGET.id();
152 WIDGET
153 .sub_event_when(&KEY_INPUT_EVENT, |a| a.state == KeyState::Pressed)
154 .sub_event(&POPUP_CLOSE_REQUESTED_EVENT)
155 .sub_event_when(&FOCUS_CHANGED_EVENT, move |a| a.is_focus_leave(id) && a.new_focus.is_some());
156 }
157 UiNodeOp::Deinit => {
158 close_timer = None;
159 }
160 UiNodeOp::Info { info } => {
161 let parent_ctx = Some(parent.unwrap_or_else(|| WIDGET.id()));
163 super::sub::SUB_MENU_PARENT_CTX.with_context(&mut Some(Arc::new(parent_ctx)), || c.info(info));
164 info.set_meta(*super::sub::SUB_MENU_POPUP_ID, super::sub::SubMenuPopupInfo { parent });
165 }
166 UiNodeOp::Update { updates } => {
167 c.update(updates);
168
169 if let Some(t) = &close_timer
170 && t.get().has_elapsed()
171 {
172 close_timer = None;
173 POPUP.force_close_id(WIDGET.id());
174 }
175
176 KEY_INPUT_EVENT.each_update(false, |args| {
177 if let KeyState::Pressed = args.state {
178 match &args.key {
179 Key::Escape => {
180 let info = WIDGET.info();
181 if let Some(m) = info.submenu_parent() {
182 args.propagation.stop();
183
184 FOCUS.focus_widget(m.id(), true);
185 POPUP.force_close_id(info.id());
186 }
187 }
188 Key::ArrowLeft | Key::ArrowRight => {
189 if let Some(info) = WINDOW.info().get(args.target.widget_id()) {
190 let info = info.into_focus_info(true, true);
191 if info.focusable_left().is_none() && info.focusable_right().is_none() {
192 if let Some(m) = info.info().submenu_parent() {
194 let mut escape = false;
195 if m.submenu_parent().is_some()
196 && let Some(o) = m.orientation_from(info.info().center())
197 {
198 escape = match o {
199 Orientation2D::Left => args.key == Key::ArrowLeft,
200 Orientation2D::Right => args.key == Key::ArrowRight,
201 Orientation2D::Below | Orientation2D::Above => false,
202 };
203 }
204
205 if escape {
206 args.propagation.stop();
207 FOCUS.focus_widget(m.id(), true);
210 POPUP.force_close_id(WIDGET.id());
211 } else if let Some(m) = info.info().submenu_root() {
212 args.propagation.stop();
213 let m = m.into_focus_info(true, true);
216 let next_root = match &args.key {
217 Key::ArrowLeft => m.next_left(),
218 Key::ArrowRight => m.next_right(),
219 _ => unreachable!(),
220 };
221 if let Some(n) = next_root {
222 FOCUS.focus_widget(n.info().id(), true);
223 }
224 }
225 }
226 }
227 }
228 }
229 _ => {}
230 }
231 }
232 });
233
234 POPUP_CLOSE_REQUESTED_EVENT.each_update(false, |args| {
235 let sub_self = if parent.is_some() {
236 WIDGET.info().submenu_parent()
237 } else {
238 Some(WIDGET.info())
240 };
241 if let Some(sub_self) = sub_self {
242 let mut close_ancestors = Some(None);
243
244 if let Some(focused) = FOCUS.focused().get()
245 && let Some(focused) = sub_self.tree().get(focused.widget_id())
246 && let Some(sub_focused) = focused.submenu_parent()
247 {
248 if sub_focused.submenu_ancestors().any(|a| a.id() == sub_self.id()) {
249 args.propagation.stop();
251 close_ancestors = None;
252 } else if sub_self.submenu_ancestors().any(|a| a.id() == sub_focused.id()) {
253 if Some(sub_focused.id()) == sub_self.submenu_parent().map(|s| s.id()) {
254 args.propagation.stop();
256 close_ancestors = None;
257 } else {
258 close_ancestors = Some(Some(sub_focused.id()));
259 }
260 }
261 }
262
263 if let Some(sub_parent_focused) = close_ancestors {
264 for a in sub_self.submenu_ancestors() {
266 if Some(a.id()) == sub_parent_focused {
267 break;
268 }
269
270 if let Some(v) = a.is_submenu_open() {
271 if v.get() {
272 POPUP_CLOSE_CMD.scoped(a.id()).notify();
274 }
275 } else if a.menu().is_none() {
276 POPUP_CLOSE_CMD.scoped(a.id()).notify_param(PopupCloseMode::Force);
278 }
279 }
280 }
281 }
282 });
283
284 FOCUS_CHANGED_EVENT.each_update(true, |args| {
285 if args.is_focus_leave(WIDGET.id())
286 && let Some(f) = &args.new_focus
287 {
288 let info = WIDGET.info();
289 let sub_self = if parent.is_some() {
290 info.submenu_parent()
291 } else {
292 Some(info.clone())
294 };
295 if let Some(sub_menu) = sub_self
296 && let Some(f) = info.tree().get(f.widget_id())
297 && !f.submenu_self_and_ancestors().any(|s| s.id() == sub_menu.id())
298 {
299 let t = TIMERS.deadline(HOVER_OPEN_DELAY_VAR.get());
306 t.subscribe(UpdateOp::Update, info.id()).perm();
307 close_timer = Some(t);
308 }
309 }
310 });
311 }
312 _ => {}
313 })
314}