ribir_core/builtin_widgets/
global_anchor.rs

1use std::rc::Rc;
2
3use crate::{prelude::*, ticker::FrameMsg};
4
5#[derive(Query, Default)]
6pub struct GlobalAnchor {
7  pub global_anchor: Anchor,
8}
9
10impl Declare for GlobalAnchor {
11  type Builder = FatObj<()>;
12  #[inline]
13  fn declarer() -> Self::Builder { FatObj::new(()) }
14}
15
16impl ComposeChild for GlobalAnchor {
17  type Child = Widget;
18  #[inline]
19  fn compose_child(this: impl StateWriter<Value = Self>, child: Self::Child) -> impl WidgetBuilder {
20    fn_widget! {
21      let wnd = ctx!().window();
22      let tick_of_layout_ready = wnd
23        .frame_tick_stream()
24        .filter(|msg| matches!(msg, FrameMsg::LayoutReady(_)));
25
26      let mut child = @$child {};
27      let wid = child.lazy_id();
28      let u = watch!(($this.get_global_anchor(), $child.layout_size()))
29        .sample(tick_of_layout_ready)
30        .subscribe(move |(_, size)| {
31          let wnd_size = wnd.size();
32          let base = wnd.map_to_global(Point::zero(), wid.assert_id());
33          // The global anchor may change during sampling, so we need to retrieve it again.
34          let Anchor {x, y} = $this.get_global_anchor();
35          let anchor = Anchor {
36            x: x.map(|x| match x {
37              HAnchor::Left(x) => x - base.x,
38              HAnchor::Right(x) => (wnd_size.width - x) - size.width - base.x,
39            }).map(HAnchor::Left),
40            y: y.map(|y| match y {
41              VAnchor::Top(y) => y - base.y,
42              VAnchor::Bottom(y) => (wnd_size.height - y) - size.height - base.y,
43            }).map(VAnchor::Top),
44          };
45          if $child.anchor != anchor {
46            $child.write().anchor = anchor;
47          }
48        });
49
50      @ $child { on_disposed: move |_| { u.unsubscribe(); } }
51    }
52  }
53}
54
55impl GlobalAnchor {
56  fn get_global_anchor(&self) -> Anchor { self.global_anchor }
57}
58
59fn bind_h_anchor(
60  this: &impl StateWriter<Value = GlobalAnchor>, wnd: &Rc<Window>, relative: HAnchor, base: f32,
61) {
62  let size = wnd.size();
63  let anchor = match relative {
64    HAnchor::Left(x) => HAnchor::Left(base + x),
65    HAnchor::Right(x) => HAnchor::Right((size.width - base) + x),
66  };
67  if this.read().global_anchor.x != Some(anchor) {
68    this.write().global_anchor.x = Some(anchor);
69  }
70}
71
72fn bind_v_anchor(
73  this: &impl StateWriter<Value = GlobalAnchor>, wnd: &Rc<Window>, relative: VAnchor, base: f32,
74) {
75  let size = wnd.size();
76  let anchor = match relative {
77    VAnchor::Top(x) => VAnchor::Top(base + x),
78    VAnchor::Bottom(x) => VAnchor::Bottom((size.height - base) + x),
79  };
80  if this.read().global_anchor.y != Some(anchor) {
81    this.write().global_anchor.y = Some(anchor);
82  }
83}
84
85impl<W> FatObj<W> {
86  /// Anchor the widget's horizontal position by placing its left edge right to
87  /// the left edge of the specified widget (`wid`) with the given relative
88  /// pixel value (`relative`).
89  pub fn left_align_to(
90    &mut self, wid: &LazyWidgetId, offset: f32, ctx: &BuildCtx,
91  ) -> impl Subscription {
92    let this = self.get_global_anchor_widget().clone_writer();
93    let wnd = ctx.window();
94    let wid = wid.clone();
95    let tick_of_layout_ready = wnd
96      .frame_tick_stream()
97      .filter(|msg| matches!(msg, FrameMsg::LayoutReady(_)));
98    tick_of_layout_ready.subscribe(move |_| {
99      let base = wnd
100        .map_to_global(Point::zero(), wid.assert_id())
101        .x;
102      bind_h_anchor(&this, &wnd, HAnchor::Left(offset), base);
103    })
104  }
105
106  /// Anchor the widget's horizontal position by placing its right edge left to
107  /// the right edge of the specified widget (`wid`) with the given relative
108  /// pixel value (`relative`).
109  pub fn right_align_to(
110    &mut self, wid: &LazyWidgetId, relative: f32, ctx: &BuildCtx,
111  ) -> impl Subscription {
112    let this = self.get_global_anchor_widget().clone_writer();
113    let wnd = ctx.window();
114    let wid = wid.clone();
115    let tick_of_layout_ready = wnd
116      .frame_tick_stream()
117      .filter(|msg| matches!(msg, FrameMsg::LayoutReady(_)));
118    tick_of_layout_ready.subscribe(move |_| {
119      let base = wnd
120        .map_to_global(Point::zero(), wid.assert_id())
121        .x;
122      let size = wnd
123        .layout_size(wid.assert_id())
124        .unwrap_or_default();
125      bind_h_anchor(&this, &wnd, HAnchor::Right(relative), base + size.width);
126    })
127  }
128
129  /// Anchors the widget's vertical position by placing its top edge below the
130  /// top edge of the specified widget (`wid`) with the given relative pixel
131  /// value (`relative`).
132  pub fn top_align_to(
133    &mut self, wid: &LazyWidgetId, relative: f32, ctx: &BuildCtx,
134  ) -> impl Subscription {
135    let this = self.get_global_anchor_widget().clone_writer();
136    let wnd = ctx.window();
137    let wid = wid.clone();
138    let tick_of_layout_ready = wnd
139      .frame_tick_stream()
140      .filter(|msg| matches!(msg, FrameMsg::LayoutReady(_)));
141    tick_of_layout_ready.subscribe(move |_| {
142      let base = wnd
143        .map_to_global(Point::zero(), wid.assert_id())
144        .y;
145      bind_v_anchor(&this, &wnd, VAnchor::Top(relative), base);
146    })
147  }
148
149  /// Anchors the widget's vertical position by placing its bottom edge above
150  /// the bottom edge of the specified widget (`wid`) with the given relative
151  /// pixel value (`relative`).
152  pub fn bottom_align_to(
153    &mut self, wid: &LazyWidgetId, relative: f32, ctx: &BuildCtx,
154  ) -> impl Subscription {
155    let this = self.get_global_anchor_widget().clone_writer();
156    let wnd = ctx.window();
157    let wid = wid.clone();
158    let tick_of_layout_ready = wnd
159      .frame_tick_stream()
160      .filter(|msg| matches!(msg, FrameMsg::LayoutReady(_)));
161    tick_of_layout_ready.subscribe(move |_| {
162      let base = wnd
163        .map_to_global(Point::zero(), wid.assert_id())
164        .y;
165      let size = wnd
166        .layout_size(wid.assert_id())
167        .unwrap_or_default();
168      bind_v_anchor(&this, &wnd, VAnchor::Bottom(relative), base + size.height);
169    })
170  }
171}
172#[cfg(test)]
173mod tests {
174  use ribir_dev_helper::*;
175
176  use super::*;
177  use crate::test_helper::*;
178
179  const WND_SIZE: Size = Size::new(100., 100.);
180  fn global_anchor() -> impl WidgetBuilder {
181    fn_widget! {
182      let parent = @MockBox {
183        anchor: Anchor::left_top(10., 10.),
184        size: Size::new(50., 50.),
185      };
186      let wid = parent.lazy_host_id();
187      let mut top_left = @MockBox {
188        size: Size::new(10., 10.),
189      };
190      top_left.left_align_to(&wid, 20., ctx!());
191      top_left.top_align_to(&wid, 10., ctx!());
192
193      let mut bottom_right = @MockBox {
194        size: Size::new(10., 10.),
195      };
196      bottom_right.right_align_to(&wid, 10.,  ctx!());
197      bottom_right.bottom_align_to(&wid, 20., ctx!());
198      @ $parent {
199        @MockStack {
200          child_pos: vec![Point::new(0., 0.), Point::new(0., 0.)],
201          @ { top_left }
202          @ { bottom_right }
203        }
204      }
205    }
206  }
207
208  widget_layout_test!(
209    global_anchor,
210    wnd_size = WND_SIZE,
211    { path = [0, 0, 0, 0, 0], x == 20.,}
212    { path = [0, 0, 0, 0, 0], y == 10.,}
213
214    { path = [0, 0, 0, 1, 0], x == 30.,}
215    { path = [0, 0, 0, 1, 0], y == 20.,}
216  );
217}