zng_wgt_menu/
popup.rs
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 style_base_fn = style_fn!(|_| DefaultStyle!());
29
30 capture_pointer_on_init = CaptureMode::Subtree;
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_list(0).clone())
44 .unwrap_or_else(|| ArcNodeList::new(ui_vec![].boxed()));
45
46 wgt.set_child(sub_menu_popup_node(children, id));
47 });
48 }
49}
50impl_style_fn!(SubMenuPopup);
51
52#[property(CHILD, capture, default(ui_vec![]), widget_impl(SubMenuPopup))]
54pub fn children(children: impl UiNodeList) {}
55
56#[property(CONTEXT, capture, widget_impl(SubMenuPopup))]
58pub fn parent_id(submenu_id: impl IntoValue<WidgetId>) {}
59
60context_var! {
61 pub static PANEL_FN_VAR: WidgetFn<zng_wgt_panel::PanelArgs> = WidgetFn::new(default_panel_fn);
67}
68
69#[property(CONTEXT, default(PANEL_FN_VAR), widget_impl(SubMenuPopup))]
73pub fn panel_fn(child: impl UiNode, panel: impl IntoVar<WidgetFn<zng_wgt_panel::PanelArgs>>) -> impl UiNode {
74 with_context_var(child, PANEL_FN_VAR, panel)
75}
76
77#[widget($crate::popup::DefaultStyle)]
79pub struct DefaultStyle(zng_wgt_layer::popup::DefaultStyle);
80impl DefaultStyle {
81 fn widget_intrinsic(&mut self) {
82 widget_set! {
83 self;
84
85 super::sub::style_fn = style_fn!(|_| super::sub::SubMenuStyle!());
86
87 base_color = light_dark(rgb(0.82, 0.82, 0.82), rgb(0.18, 0.18, 0.18));
88 background_color = BASE_COLOR_VAR.rgba();
89 border = {
90 widths: 1,
91 sides: BASE_COLOR_VAR.shade_into(1),
92 };
93 }
94 }
95}
96
97pub fn default_panel_fn(args: zng_wgt_panel::PanelArgs) -> impl UiNode {
101 let scroll_id = WidgetId::new_unique();
103 let _ = zng_wgt_scroll::cmd::SCROLL_UP_CMD
104 .scoped(scroll_id)
105 .shortcut()
106 .set(Shortcuts::new());
107 let _ = zng_wgt_scroll::cmd::SCROLL_DOWN_CMD
108 .scoped(scroll_id)
109 .shortcut()
110 .set(Shortcuts::new());
111
112 zng_wgt_scroll::Scroll! {
113 id = scroll_id;
114 focusable = false;
115 child = Stack! {
116 children = args.children;
117 direction = zng_wgt_stack::StackDirection::top_to_bottom();
118 };
119 mode = zng_wgt_scroll::ScrollMode::VERTICAL;
120 }
121}
122
123pub fn sub_menu_popup_node(children: ArcNodeList<BoxedUiNodeList>, parent: Option<WidgetId>) -> impl UiNode {
125 let child = zng_wgt_panel::node(
126 children,
127 if parent.is_none() {
128 super::context::PANEL_FN_VAR
129 } else {
130 PANEL_FN_VAR
131 },
132 );
133 let mut close_timer = None;
134 match_node(child, move |c, op| match op {
135 UiNodeOp::Init => {
136 WIDGET
137 .sub_event(&KEY_INPUT_EVENT)
138 .sub_event(&POPUP_CLOSE_REQUESTED_EVENT)
139 .sub_event(&FOCUS_CHANGED_EVENT);
140 }
141 UiNodeOp::Deinit => {
142 close_timer = None;
143 }
144 UiNodeOp::Info { info } => {
145 let parent_ctx = Some(parent.unwrap_or_else(|| WIDGET.id()));
147 super::sub::SUB_MENU_PARENT_CTX.with_context(&mut Some(Arc::new(parent_ctx)), || c.info(info));
148 info.set_meta(*super::sub::SUB_MENU_POPUP_ID, super::sub::SubMenuPopupInfo { parent });
149 }
150 UiNodeOp::Event { update } => {
151 c.event(update);
152
153 if let Some(args) = KEY_INPUT_EVENT.on_unhandled(update) {
154 if let KeyState::Pressed = args.state {
155 match &args.key {
156 Key::Escape => {
157 let info = WIDGET.info();
158 if let Some(m) = info.submenu_parent() {
159 args.propagation().stop();
160
161 FOCUS.focus_widget(m.id(), true);
162 POPUP.force_close_id(info.id());
163 }
164 }
165 Key::ArrowLeft | Key::ArrowRight => {
166 if let Some(info) = WINDOW.info().get(args.target.widget_id()) {
167 let info = info.into_focus_info(true, true);
168 if info.focusable_left().is_none() && info.focusable_right().is_none() {
169 if let Some(m) = info.info().submenu_parent() {
171 let mut escape = false;
172 if m.submenu_parent().is_some() {
173 if let Some(o) = m.orientation_from(info.info().center()) {
174 escape = match o {
175 Orientation2D::Left => args.key == Key::ArrowLeft,
176 Orientation2D::Right => args.key == Key::ArrowRight,
177 Orientation2D::Below | Orientation2D::Above => false,
178 };
179 }
180 }
181
182 if escape {
183 args.propagation().stop();
184 FOCUS.focus_widget(m.id(), true);
187 POPUP.force_close_id(WIDGET.id());
188 } else if let Some(m) = info.info().submenu_root() {
189 args.propagation().stop();
190 let m = m.into_focus_info(true, true);
193 let next_root = match &args.key {
194 Key::ArrowLeft => m.next_left(),
195 Key::ArrowRight => m.next_right(),
196 _ => unreachable!(),
197 };
198 if let Some(n) = next_root {
199 FOCUS.focus_widget(n.info().id(), true);
200 }
201 }
202 }
203 }
204 }
205 }
206 _ => {}
207 }
208 }
209 } else if let Some(args) = POPUP_CLOSE_REQUESTED_EVENT.on_unhandled(update) {
210 let sub_self = if parent.is_some() {
211 WIDGET.info().submenu_parent()
212 } else {
213 Some(WIDGET.info())
215 };
216 if let Some(sub_self) = sub_self {
217 let mut close_ancestors = Some(None);
218
219 if let Some(focused) = FOCUS.focused().get() {
220 if let Some(focused) = sub_self.tree().get(focused.widget_id()) {
221 if let Some(sub_focused) = focused.submenu_parent() {
222 if sub_focused.submenu_ancestors().any(|a| a.id() == sub_self.id()) {
223 args.propagation().stop();
225 close_ancestors = None;
226 } else if sub_self.submenu_ancestors().any(|a| a.id() == sub_focused.id()) {
227 if Some(sub_focused.id()) == sub_self.submenu_parent().map(|s| s.id()) {
228 args.propagation().stop();
230 close_ancestors = None;
231 } else {
232 close_ancestors = Some(Some(sub_focused.id()));
233 }
234 }
235 }
236 }
237 }
238
239 if let Some(sub_parent_focused) = close_ancestors {
240 for a in sub_self.submenu_ancestors() {
242 if Some(a.id()) == sub_parent_focused {
243 break;
244 }
245
246 if let Some(v) = a.is_submenu_open() {
247 if v.get() {
248 POPUP_CLOSE_CMD.scoped(a.id()).notify();
250 }
251 } else if a.menu().is_none() {
252 POPUP_CLOSE_CMD.scoped(a.id()).notify_param(PopupCloseMode::Force);
254 }
255 }
256 }
257 }
258 } else if let Some(args) = FOCUS_CHANGED_EVENT.on(update) {
259 if args.is_focus_leave(WIDGET.id()) {
260 if let Some(f) = &args.new_focus {
261 let info = WIDGET.info();
262 let sub_self = if parent.is_some() {
263 info.submenu_parent()
264 } else {
265 Some(info.clone())
267 };
268 if let (Some(sub_menu), Some(f)) = (sub_self, info.tree().get(f.widget_id())) {
269 if !f.submenu_self_and_ancestors().any(|s| s.id() == sub_menu.id()) {
270 let t = TIMERS.deadline(HOVER_OPEN_DELAY_VAR.get());
277 t.subscribe(UpdateOp::Update, info.id()).perm();
278 close_timer = Some(t);
279 }
280 }
281 }
282 }
283 }
284 }
285 UiNodeOp::Update { .. } => {
286 if let Some(t) = &close_timer {
287 if t.get().has_elapsed() {
288 close_timer = None;
289 POPUP.force_close_id(WIDGET.id());
290 }
291 }
292 }
293 _ => {}
294 })
295}