Skip to main content

ribir_core/
overlay.rs

1use std::{cell::RefCell, mem::replace, rc::Rc};
2
3use ribir_macros::Query;
4
5use crate::prelude::*;
6
7#[derive(Clone)]
8pub struct OverlayStyle {
9  pub close_policy: ClosePolicy,
10  pub mask_brush: Option<Brush>,
11}
12
13bitflags! {
14  #[derive(Clone, Copy)]
15  pub struct ClosePolicy: u8 {
16    const NONE = 0b0000;
17    const ESC = 0b0001;
18    const TAP_OUTSIDE = 0b0010;
19  }
20}
21
22impl CustomStyle for OverlayStyle {
23  fn default_style(_: &BuildCtx) -> Self {
24    Self {
25      close_policy: ClosePolicy::ESC | ClosePolicy::TAP_OUTSIDE,
26      mask_brush: Some(Color::from_f32_rgba(0.3, 0.3, 0.3, 0.3).into()),
27    }
28  }
29}
30
31/// A handle to close the overlay
32#[derive(Clone)]
33pub struct OverlayCloseHandle(OverlayState);
34impl OverlayCloseHandle {
35  pub fn close(&self) { self.0.close() }
36}
37
38struct OverlayData {
39  builder: Box<dyn Fn(OverlayCloseHandle) -> BoxedWidget>,
40  style: RefCell<Option<OverlayStyle>>,
41  state: OverlayState,
42}
43
44#[derive(Clone)]
45pub struct Overlay(Rc<OverlayData>);
46
47impl Overlay {
48  /// Create overlay from Clone able widget.
49  ///
50  /// ### Example
51  ///  ``` no_run
52  ///  use ribir::prelude::*;
53  ///  let w = fn_widget! {
54  ///  let overlay = Overlay::new(
55  ///     fn_widget! {
56  ///       @Text {
57  ///         h_align: HAlign::Center,
58  ///         v_align: VAlign::Center,
59  ///         text: "Hello"
60  ///       }
61  ///     }
62  ///   );
63  ///   @FilledButton{
64  ///     on_tap: move |e| overlay.show(e.window()),
65  ///     @{ Label::new("Click to show overlay") }
66  ///   }
67  ///  };
68  ///  App::run(w);
69  /// ```
70  pub fn new<M>(widget: M) -> Self
71  where
72    M: WidgetBuilder + 'static + Clone,
73  {
74    Self(Rc::new(OverlayData {
75      builder: Box::new(move |_| widget.clone().box_it()),
76      style: RefCell::new(None),
77      state: OverlayState::default(),
78    }))
79  }
80
81  /// Create overlay from a builder with a close_handle
82  ///
83  /// ### Example
84  /// popup a widget of a button which will close when clicked.
85  /// ``` no_run
86  /// use ribir::prelude::*;
87  /// let w = fn_widget! {
88  ///   let overlay = Overlay::new_with_handle(
89  ///     move |ctrl: OverlayCloseHandle| {
90  ///       let ctrl = ctrl.clone();
91  ///       fn_widget! {
92  ///         @FilledButton {
93  ///           h_align: HAlign::Center,
94  ///           v_align: VAlign::Center,
95  ///           on_tap: move |_| ctrl.close(),
96  ///           @{ Label::new("Click to close") }
97  ///         }
98  ///       }
99  ///     }
100  ///   );
101  ///   @FilledButton {
102  ///     on_tap: move |e| overlay.show(e.window()),
103  ///     @{ Label::new("Click to show overlay") }
104  ///   }
105  /// };
106  ///
107  /// App::run(w).with_size(Size::new(200., 200.));
108  /// ```
109  pub fn new_with_handle<O, M>(builder: M) -> Self
110  where
111    M: Fn(OverlayCloseHandle) -> O + 'static,
112    O: WidgetBuilder + 'static,
113  {
114    Self(Rc::new(OverlayData {
115      builder: Box::new(move |ctrl| builder(ctrl).box_it()),
116      style: RefCell::new(None),
117      state: OverlayState::default(),
118    }))
119  }
120
121  /// Overlay will show with the given style, if the overlay have not been set
122  /// with style, the default style will be get from the theme.
123  pub fn with_style(&self, style: OverlayStyle) { *self.0.style.borrow_mut() = Some(style); }
124
125  /// the Overlay widget will be show at the top level of all widget.
126  /// if the overlay is showing, nothing will happen.
127  pub fn show(&self, wnd: Rc<Window>) {
128    if self.is_show() {
129      return;
130    }
131    let w = (self.0.builder)(self.0.state.close_handle());
132    let style = self.0.style.borrow().clone();
133    self.0.state.show(w, style, wnd);
134  }
135
136  /// User can make transform before the widget show at the top level of all
137  /// widget. if the overlay is showing, nothing will happen.
138  ///
139  /// ### Example
140  /// Overlay widget which auto align horizontal position to the src button even
141  /// when window's size changed
142  /// ``` no_run
143  /// use ribir::prelude::*;
144  /// let w = fn_widget! {
145  ///   let overlay = Overlay::new(
146  ///     fn_widget! { @Text { text: "overlay" } }
147  ///   );
148  ///   let button = @FilledButton{};
149  ///   let wid = button.lazy_host_id();
150  ///   @$button {
151  ///     h_align: HAlign::Center,
152  ///     v_align: VAlign::Center,
153  ///     on_tap: move |e| {
154  ///       let wid = wid.clone();
155  ///       overlay.show_map(
156  ///         move |w, _| {
157  ///           let wid = wid.clone();
158  ///           fn_widget! {
159  ///             let mut w = @$w {};
160  ///             w.left_align_to(&wid, 0., ctx!());
161  ///             w
162  ///           }
163  ///         },
164  ///         e.window()
165  ///       );
166  ///     },
167  ///     @{ Label::new("Click to show overlay") }
168  ///   }
169  /// };
170  /// App::run(w);
171  /// ```
172  pub fn show_map<O, F>(&self, f: F, wnd: Rc<Window>)
173  where
174    F: Fn(BoxedWidget, OverlayCloseHandle) -> O + 'static,
175    O: WidgetBuilder + 'static,
176  {
177    if self.is_show() {
178      return;
179    }
180
181    let close_handle = self.0.state.close_handle();
182    let w = f((self.0.builder)(close_handle.clone()), close_handle);
183    let style = self.0.style.borrow().clone();
184    self.0.state.show(w, style, wnd);
185  }
186
187  /// Show the widget at the give position.
188  /// if the overlay is showing, nothing will happen.
189  pub fn show_at(&self, pos: Point, wnd: Rc<Window>) {
190    if self.is_show() {
191      return;
192    }
193    self.show_map(
194      move |w, _| {
195        fn_widget! {
196          @$w { anchor: Anchor::from_point(pos) }
197        }
198      },
199      wnd,
200    );
201  }
202
203  /// return whether the overlay is show.
204  pub fn is_show(&self) -> bool { self.0.state.is_show() }
205
206  /// remove the showing overlay.
207  pub fn close(&self) { self.0.state.close() }
208}
209
210enum OverlayInnerState {
211  ToShow(Instant, Rc<Window>),
212  Showing(WidgetId, Rc<Window>),
213  Hided,
214}
215
216#[derive(Clone)]
217struct OverlayState(Rc<RefCell<OverlayInnerState>>);
218impl Default for OverlayState {
219  fn default() -> Self { OverlayState(Rc::new(RefCell::new(OverlayInnerState::Hided))) }
220}
221
222impl OverlayState {
223  fn close(&self) {
224    let state = replace(&mut *self.0.borrow_mut(), OverlayInnerState::Hided);
225    if let OverlayInnerState::Showing(wid, wnd) = state {
226      let _ = AppCtx::spawn_local(async move {
227        let root = wnd.widget_tree.borrow().root();
228        wid.dispose_subtree(&mut wnd.widget_tree.borrow_mut());
229        wnd.widget_tree.borrow_mut().mark_dirty(root);
230      });
231    }
232  }
233
234  fn is_show(&self) -> bool { !matches!(*self.0.borrow(), OverlayInnerState::Hided) }
235
236  fn show(&self, w: impl WidgetBuilder + 'static, style: Option<OverlayStyle>, wnd: Rc<Window>) {
237    if self.is_show() {
238      return;
239    }
240    let this = self.clone();
241    let instant = Instant::now();
242    *this.0.borrow_mut() = OverlayInnerState::ToShow(instant, wnd);
243    let _ = AppCtx::spawn_local(async move {
244      let wnd = match (instant, &*this.0.borrow()) {
245        (instant, OverlayInnerState::ToShow(crate_at, wnd)) if &instant == crate_at => wnd.clone(),
246        _ => return,
247      };
248      let build_ctx = BuildCtx::new(None, &wnd.widget_tree);
249      let style = style.unwrap_or_else(|| OverlayStyle::of(&build_ctx));
250      let w = this.wrap_style(w, style).build(&build_ctx);
251      let wid = w.id();
252      *this.0.borrow_mut() = OverlayInnerState::Showing(wid, wnd.clone());
253      let root = wnd.widget_tree.borrow().root();
254      build_ctx.append_child(root, w);
255      build_ctx.on_subtree_mounted(wid);
256      build_ctx.mark_dirty(wid);
257    });
258  }
259
260  fn wrap_style(&self, w: impl WidgetBuilder, style: OverlayStyle) -> impl WidgetBuilder {
261    let this = self.clone();
262    fn_widget! {
263      let OverlayStyle { close_policy, mask_brush } = style;
264      let this2 = this.clone();
265      @Container {
266        size: Size::new(f32::INFINITY, f32::INFINITY),
267        background: mask_brush.unwrap_or_else(|| Color::from_u32(0).into()),
268        on_tap: move |e| {
269          if close_policy.contains(ClosePolicy::TAP_OUTSIDE)
270            && e.target() == e.current_target() {
271            this.close();
272          }
273        },
274        on_key_down: move |e| {
275          if close_policy.contains(ClosePolicy::ESC)
276            && *e.key() == VirtualKey::Named(NamedKey::Escape) {
277            this2.close();
278          }
279        },
280        @$w {}
281      }
282    }
283  }
284
285  fn close_handle(&self) -> OverlayCloseHandle { OverlayCloseHandle(self.clone()) }
286}
287
288#[derive(Query)]
289pub(crate) struct OverlayRoot {}
290
291impl Render for OverlayRoot {
292  fn perform_layout(&self, clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size {
293    let mut size = ZERO_SIZE;
294    let mut layouter = ctx.first_child_layouter();
295    while let Some(mut l) = layouter {
296      let child_size = l.perform_widget_layout(clamp);
297      size = size.max(child_size);
298      layouter = l.into_next_sibling();
299    }
300    size
301  }
302
303  fn paint(&self, _: &mut PaintingCtx) {}
304}
305
306#[cfg(test)]
307mod tests {
308  use std::{cell::RefCell, rc::Rc};
309
310  use ribir_dev_helper::assert_layout_result_by_path;
311
312  use crate::{prelude::*, reset_test_env, test_helper::*};
313
314  #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
315  #[test]
316  fn overlay() {
317    reset_test_env!();
318    let size = Size::zero();
319    let widget = fn_widget! {
320      @MockBox {
321        size,
322        @MockBox { size }
323      }
324    };
325
326    let mut wnd = TestWindow::new(widget);
327    let w_log = Rc::new(RefCell::new(vec![]));
328    let r_log = w_log.clone();
329    let overlay = Overlay::new(fn_widget! {
330      @MockBox {
331        size,
332        on_mounted: {
333          let w_log = w_log.clone();
334          move |_| { w_log.borrow_mut().push("mounted");}
335        },
336        on_disposed: move |_| { w_log.borrow_mut().push("disposed");}
337      }
338    });
339    wnd.draw_frame();
340
341    let root = wnd.widget_tree.borrow().root();
342    assert_eq!(wnd.widget_tree.borrow().count(root), 3);
343
344    overlay.show(wnd.0.clone());
345    overlay.close();
346    overlay.show_at(Point::new(50., 30.), wnd.0.clone());
347    wnd.draw_frame();
348    assert_eq!(*r_log.borrow(), &["mounted"]);
349    // the path [1, 0, 0, 0] is from root to anchor,
350    // OverlayRoot -> BoxDecoration-> Container -> Anchor
351    assert_layout_result_by_path!(wnd, {path = [1, 0, 0, 0], x == 50., y == 30.,});
352
353    overlay.close();
354    wnd.draw_frame();
355    assert_eq!(*r_log.borrow(), &["mounted", "disposed"]);
356    assert_eq!(wnd.widget_tree.borrow().count(root), 3);
357  }
358}