telar_ui_core/
layout_item.rs1use std::cell::RefCell;
2use std::rc::Rc;
3
4use geometry_core::Rect;
5use layout_core::{LayoutError, LayoutStyle, NodeId};
6use platform_core::Event;
7use reactive_core::RwSignal;
8use ui_tree::{Component, EventResult, RenderNode, Segment};
9
10use crate::context::{new_container, track_layout};
11use crate::layout_leaf::LayoutLeaf;
12
13#[derive(Clone)]
19pub(crate) struct Child {
20 pub(crate) item: Rc<RefCell<Box<dyn LayoutItem>>>,
21 pub(crate) rect: Option<RwSignal<Rect>>,
22 pub(crate) segment: Rc<Segment>,
23}
24
25impl Child {
26 pub(crate) fn node(&self) -> layout_core::NodeId {
28 self.item.borrow().layout_node()
29 }
30}
31
32pub(crate) fn make_child(widget: Box<dyn LayoutItem>) -> Child {
36 let rect = track_layout(widget.layout_node());
37 let item = Rc::new(RefCell::new(widget));
38 let segment = mount_item_segment(Rc::clone(&item));
39 Child {
40 item,
41 rect,
42 segment,
43 }
44}
45
46pub(crate) type TrackedChildren = Vec<Child>;
47
48pub(crate) fn mount_item_segment(item: Rc<RefCell<Box<dyn LayoutItem>>>) -> Rc<Segment> {
52 let name = item
53 .try_borrow()
54 .map(|i| i.debug_name())
55 .unwrap_or("Component");
56 Segment::mount_fn_named(name, move || item.try_borrow().ok().map(|i| i.view()))
57}
58
59pub(crate) trait LeafWidget {
60 fn layout_leaf(&self) -> &LayoutLeaf;
61}
62
63pub trait LayoutItem: Component {
64 fn layout_node(&self) -> NodeId;
65}
66
67pub struct ClippedItem {
78 inner: Box<dyn LayoutItem>,
79 rect: RwSignal<Rect>,
80 axis: ClipAxis,
81}
82
83#[derive(Clone, Copy, PartialEq, Eq, Debug)]
85pub enum ClipAxis {
86 Both,
88 Horizontal,
90 Vertical,
92}
93
94const UNBOUNDED: f32 = 1.0e6;
98
99impl ClippedItem {
100 pub fn new(inner: Box<dyn LayoutItem>) -> Self {
101 Self::along(inner, ClipAxis::Both)
102 }
103
104 pub fn along(inner: Box<dyn LayoutItem>, axis: ClipAxis) -> Self {
111 let rect = track_layout(inner.layout_node()).expect("clipped item's node not registered");
112 Self { inner, rect, axis }
113 }
114
115 fn clip(&self) -> Rect {
116 let rect = self.rect.get();
117 match self.axis {
118 ClipAxis::Both => rect,
119 ClipAxis::Horizontal => Rect::new(rect.x, -UNBOUNDED, rect.width, UNBOUNDED * 2.0),
120 ClipAxis::Vertical => Rect::new(-UNBOUNDED, rect.y, UNBOUNDED * 2.0, rect.height),
121 }
122 }
123}
124
125impl LayoutItem for ClippedItem {
126 fn layout_node(&self) -> NodeId {
127 self.inner.layout_node()
128 }
129}
130
131impl Component for ClippedItem {
132 fn view(&self) -> RenderNode {
133 RenderNode::Clip {
134 rect: self.clip(),
135 radius: renderer_core::BorderRadius::zero(),
136 children: ui_tree::NodeVec::collect([self.inner.view()]),
137 }
138 }
139
140 fn on_event(&mut self, event: &Event) -> EventResult {
141 let outside = |x: f64, y: f64| !self.clip().contains(x as f32, y as f32);
142 match event {
143 Event::PointerPressed { x, y, .. } | Event::PointerMoved { x, y, .. }
144 if outside(*x, *y) =>
145 {
146 EventResult::Ignored
147 }
148 _ => self.inner.on_event(event),
149 }
150 }
151
152 fn debug_name(&self) -> &'static str {
153 "Clipped"
154 }
155}
156
157impl<T: LeafWidget + Component> LayoutItem for T {
158 fn layout_node(&self) -> NodeId {
159 self.layout_leaf().node
160 }
161}
162
163impl Component for Box<dyn LayoutItem> {
165 fn view(&self) -> RenderNode {
166 (**self).view()
167 }
168
169 fn on_event(&mut self, event: &Event) -> EventResult {
170 (**self).on_event(event)
171 }
172
173 fn debug_name(&self) -> &'static str {
174 (**self).debug_name()
175 }
176}
177
178impl LayoutItem for Box<dyn LayoutItem> {
179 fn layout_node(&self) -> NodeId {
180 (**self).layout_node()
181 }
182}
183
184pub fn box_item(item: impl LayoutItem + 'static) -> Box<dyn LayoutItem> {
186 Box::new(item)
187}
188
189pub(crate) fn register_container(
190 layout_style: LayoutStyle,
191 children: Vec<Box<dyn LayoutItem>>,
192) -> Result<(NodeId, RwSignal<Rect>, TrackedChildren), LayoutError> {
193 let child_nodes = children.iter().map(|c| c.layout_node()).collect::<Vec<_>>();
194 let node = new_container(layout_style, &child_nodes)?;
195 let rect = track_layout(node).expect("new_container always registers a signal");
196 let children = children.into_iter().map(make_child).collect();
197 Ok((node, rect, children))
198}
199
200#[macro_export]
202macro_rules! impl_leaf_widget {
203 ($struct:ident) => {
204 impl $crate::layout_item::LeafWidget for $struct {
205 fn layout_leaf(&self) -> &$crate::layout_leaf::LayoutLeaf {
206 &self.leaf
207 }
208 }
209 };
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::StyledContainer;
216 use crate::context::{compute_layout, reset_layout_runtime};
217 use layout_core::AvailableSpace;
218 use platform_core::{PointerButton, PointerSource};
219 use renderer_core::RectStyle;
220 use std::cell::Cell;
221 use std::rc::Rc;
222
223 fn press(x: f64, y: f64) -> Event {
224 Event::PointerPressed {
225 x,
226 y,
227 button: PointerButton::Primary,
228 source: PointerSource::Mouse,
229 }
230 }
231
232 fn release(x: f64, y: f64) -> Event {
233 Event::PointerReleased {
234 x,
235 y,
236 button: PointerButton::Primary,
237 source: PointerSource::Mouse,
238 }
239 }
240
241 #[test]
247 fn a_press_outside_the_clip_never_reaches_what_it_hides() {
248 let pressed = Rc::new(Cell::new(false));
249 let sink = Rc::clone(&pressed);
250 reset_layout_runtime();
251 let inner = StyledContainer::new(
252 LayoutStyle::new().flex_row().width(100.0).height(20.0),
253 |_r| RectStyle::default(),
254 vec![],
255 )
256 .unwrap()
257 .on_press(move || sink.set(true));
258 let mut clipped = ClippedItem::new(Box::new(
260 StyledContainer::new(
261 LayoutStyle::new().flex_row().width(40.0).height(20.0),
262 |_r| RectStyle::default(),
263 vec![Box::new(inner)],
264 )
265 .unwrap(),
266 ));
267 compute_layout(
268 clipped.layout_node(),
269 AvailableSpace::Definite(40.0),
270 AvailableSpace::Definite(20.0),
271 )
272 .unwrap();
273
274 clipped.on_event(&press(80.0, 10.0));
275 clipped.on_event(&release(80.0, 10.0));
276 assert!(
277 !pressed.get(),
278 "a tap 40px past the clip's edge reached the item hidden behind it"
279 );
280
281 clipped.on_event(&press(10.0, 10.0));
282 clipped.on_event(&release(10.0, 10.0));
283 assert!(
284 pressed.get(),
285 "and a tap on the part that is actually drawn still has to land"
286 );
287 }
288}