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