Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/elements/stack/mod.rs
1//! Stack lets you render multiple elements "on top of each other". It lets you offset elements
2//! from the parent element or between different layers in one Stack, etc.
3//!
4//! Stacks order their elements in z-space using layers.
5//! E.g.
6//! stack 1
7//! --> start layer 1
8//! child 1
9//! <-- stop layer 1
10//! --> start layer 2
11//! stack 2
12//! --> start layer 3
13//! child 2
14//! <-- stop layer 3
15//! --> start layer 4
16//! child 3
17//! <-- stop layer 4
18//! <-- stop layer 2
19//! --> start layer 5
20//! child 4
21//! <-- stop layer 5
22//!
23//! Note that by default, all Stack's children contribute to its final size computation. For
24//! example, if you wanted to render a small square, and then a bigger translucent square that
25//! covers it.
26//! However, if you'd rather have them arranged differently, use `Stack::add_positioned_child`. The
27//! simple way of thinking about the two is that using `Stack::add_child` renders a stack as if all
28//! the layers were "merged" together, while positioned children are rendered as separate layers.
29//! More context here: https://medium.flutterdevs.com/stack-and-positioned-widget-in-flutter-3d1a7b30b09a
30 
31mod offset_positioning;
32mod overlay;
33mod positioned;
34mod save_position;
35 
36pub use offset_positioning::*;
37use overlay::Overlay;
38use pathfinder_geometry::rect::RectF;
39use positioned::*;
40pub use save_position::*;
41 
42use crate::{
43 event::DispatchedEvent,
44 text::{word_boundaries::WordBoundariesPolicy, IsRect, SelectionDirection, SelectionType},
45};
46 
47use super::{
48 AfterLayoutContext, AppContext, Element, EventContext, LayoutContext, PaintContext, Point,
49 SelectableElement, Selection, SelectionFragment, SizeConstraint,
50};
51use crate::ClipBounds;
52use log::warn;
53use pathfinder_geometry::vector::{vec2f, Vector2F};
54 
55#[derive(Clone, Copy, Default)]
56pub enum EventDispatchMode {
57 /// Current behavior: dispatch event to every child regardless of
58 /// whether a prior child already handled it.
59 #[default]
60 Broadcast,
61 /// Waterfall: stop dispatching to subsequent children once one
62 /// reports the event as handled.
63 Waterfall,
64}
65 
66struct StackChild {
67 element: Box<dyn Element>,
68 painted: bool,
69}
70 
71impl StackChild {
72 fn new(element: Box<dyn Element>) -> Self {
73 Self {
74 element,
75 painted: false,
76 }
77 }
78}
79 
80#[derive(Default)]
81pub struct Stack {
82 children: Vec<StackChild>,
83 size: Option<Vector2F>,
84 origin: Option<Point>,
85 constrain_absolute_children: bool,
86 event_dispatch_mode: EventDispatchMode,
87}
88 
89/// Since this is in the UI package, I can't access feature flags.
90/// We can flip this bool to disable this functionality if we
91/// run into any other regressions. When false, all stacks will constrain
92/// their absolute positioned children, which is behavior we'd like to move
93/// away from.
94const SHOULD_ENABLE_NEW_STACK_CONSTRAINT_BEHAVIOR: bool = true;
95 
96impl Stack {
97 pub fn new() -> Self {
98 Stack {
99 children: Default::default(),
100 size: Default::default(),
101 origin: Default::default(),
102 constrain_absolute_children: !SHOULD_ENABLE_NEW_STACK_CONSTRAINT_BEHAVIOR,
103 event_dispatch_mode: if cfg!(debug_assertions) {
104 EventDispatchMode::Waterfall
105 } else {
106 EventDispatchMode::Broadcast
107 },
108 }
109 }
110 
111 pub fn with_constrain_absolute_children(mut self) -> Self {
112 self.constrain_absolute_children = true;
113 self
114 }
115 
116 pub fn with_event_dispatch_mode(mut self, mode: EventDispatchMode) -> Self {
117 self.event_dispatch_mode = mode;
118 self
119 }
120 
121 /// Add a new child to the stack with a specific positioning.
122 pub fn with_positioned_child(
123 mut self,
124 child: Box<dyn Element>,
125 positioning: OffsetPositioning,
126 ) -> Self {
127 self.add_positioned_child(child, positioning);
128 self
129 }
130 
131 /// Add a new child to the stack with a specific positioning.
132 pub fn add_positioned_child(
133 &mut self,
134 child: Box<dyn Element>,
135 positioning: OffsetPositioning,
136 ) {
137 self.extend(Some(
138 Positioned::new(child).with_offset(positioning).finish(),
139 ));
140 }
141 
142 /// Add a new child to the stack as an overlay
143 ///
144 /// The child (and its children) will be layered above the normal UI elements. This will allow
145 /// it to float above the rest of the UI—useful for things like dropdowns and menus. The new
146 /// layer will be unclipped by default.
147 pub fn add_overlay_child(&mut self, child: Box<dyn Element>) {
148 self.extend(Some(Overlay::new(child).finish()));
149 }
150 
151 /// Add a new child to the stack as an overlay with a specific positioning
152 ///
153 /// The child (and its children) will be layered above the normal UI elements. This will allow
154 /// it to float above the rest of the UI—useful for things like dropdowns and menus. The new
155 /// layer will be unclipped by default.
156 pub fn with_positioned_overlay_child(
157 mut self,
158 child: Box<dyn Element>,
159 positioning: OffsetPositioning,
160 ) -> Self {
161 self.add_positioned_overlay_child(child, positioning);
162 self
163 }
164 
165 /// Add a new child to the stack as an overlay with a specific positioning
166 ///
167 /// The child (and its children) will be layered above the normal UI elements. This will allow
168 /// it to float above the rest of the UI—useful for things like dropdowns and menus. The new
169 /// layer will be unclipped by default.
170 pub fn add_positioned_overlay_child(
171 &mut self,
172 child: Box<dyn Element>,
173 positioning: OffsetPositioning,
174 ) {
175 self.add_positioned_child(Overlay::new(child).finish(), positioning);
176 }
177}
178 
179impl Element for Stack {
180 fn layout(
181 &mut self,
182 constraint: SizeConstraint,
183 ctx: &mut LayoutContext,
184 app: &AppContext,
185 ) -> Vector2F {
186 let mut size = constraint.min;
187 for child in &mut self.children {
188 if child
189 .element
190 .parent_data()
191 .and_then(|d| d.downcast_ref::<OffsetPositioning>())
192 .is_none()
193 {
194 // Only take child size into account if it's not an absolutely positioned element.
195 // (Absolutely positioned elements must have an `OffsetPositioning` parent_data.
196 size = size.max(child.element.layout(constraint, ctx, app));
197 }
198 }
199 
200 let absolute_constraints = if self.constrain_absolute_children {
201 constraint
202 } else {
203 SizeConstraint::new(Vector2F::zero(), ctx.window_size)
204 };
205 for child in &mut self.children {
206 if let Some(offset_positioning) = child
207 .element
208 .parent_data()
209 .and_then(|d| d.downcast_ref::<OffsetPositioning>())
210 {
211 child.element.layout(
212 offset_positioning.size_constraint(
213 size,
214 ctx.window_size,
215 absolute_constraints,
216 ctx.position_cache,
217 ),
218 ctx,
219 app,
220 );
221 }
222 }
223 
224 self.size = Some(size);
225 size
226 }
227 
228 fn after_layout(&mut self, ctx: &mut AfterLayoutContext, app: &AppContext) {
229 for child in &mut self.children {
230 child.element.after_layout(ctx, app);
231 }
232 }
233 
234 fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) {
235 self.origin = Some(Point::from_vec2f(origin, ctx.scene.z_index()));
236 let parent_rect = self.bounds().unwrap();
237 for child in &mut self.children {
238 ctx.scene.start_layer(ClipBounds::ActiveLayer);
239 ctx.position_cache.start();
240 let child_origin = if let Some(offset_positioning) = child
241 .element
242 .parent_data()
243 .and_then(|d| d.downcast_ref::<OffsetPositioning>())
244 {
245 let child_size = child.element.size().unwrap();
246 match (
247 offset_positioning.x_axis.compute_child_position(
248 child_size,
249 parent_rect,
250 ctx.window_size,
251 ctx.position_cache,
252 ),
253 offset_positioning.y_axis.compute_child_position(
254 child_size,
255 parent_rect,
256 ctx.window_size,
257 ctx.position_cache,
258 ),
259 ) {
260 (Ok(x), Ok(y)) => vec2f(x, y),
261 (x_res, y_res) => {
262 // Log a warning when position computation fails.
263 // This can happen when conditional positioning fails or when position cache
264 // doesn't have the required position data.
265 if !offset_positioning.x_axis.anchor.is_conditional()
266 && !offset_positioning.y_axis.anchor.is_conditional()
267 {
268 warn!(
269 "Failed to compute position for stack child element. Skipping child. X: {x_res:?}, Y: {y_res:?}."
270 );
271 }
272 
273 ctx.position_cache.end();
274 ctx.scene.stop_layer();
275 continue;
276 }
277 }
278 } else {
279 origin
280 };
281 
282 child.element.paint(child_origin, ctx, app);
283 child.painted = true;
284 
285 ctx.position_cache.end();
286 ctx.scene.stop_layer();
287 }
288 }
289 
290 fn dispatch_event(
291 &mut self,
292 event: &DispatchedEvent,
293 ctx: &mut EventContext,
294 app: &AppContext,
295 ) -> bool {
296 let mut handled = false;
297 
298 match self.event_dispatch_mode {
299 EventDispatchMode::Broadcast => {
300 for child in self.children.iter_mut() {
301 // We should not dispatch event to children that are not painted.
302 if child.painted {
303 handled |= child.element.dispatch_event(event, ctx, app);
304 }
305 }
306 }
307 EventDispatchMode::Waterfall => {
308 // For waterfall, we want to dispatch event to children in the reverse order (top first).
309 for child in self.children.iter_mut().rev() {
310 // We should not dispatch event to children that are not painted.
311 if child.painted && child.element.dispatch_event(event, ctx, app) {
312 return true;
313 }
314 }
315 }
316 }
317 handled
318 }
319 
320 fn size(&self) -> Option<Vector2F> {
321 self.size
322 }
323 
324 fn origin(&self) -> Option<Point> {
325 self.origin
326 }
327 
328 fn as_selectable_element(&self) -> Option<&dyn SelectableElement> {
329 Some(self as &dyn SelectableElement)
330 }
331 
332 #[cfg(any(test, feature = "test-util"))]
333 fn debug_text_content(&self) -> Option<String> {
334 let texts: Vec<String> = self
335 .children
336 .iter()
337 .filter_map(|child| child.element.debug_text_content())
338 .collect();
339 if texts.is_empty() {
340 None
341 } else {
342 Some(texts.join("\n"))
343 }
344 }
345}
346 
347impl SelectableElement for Stack {
348 fn get_selection(
349 &self,
350 selection_start: Vector2F,
351 selection_end: Vector2F,
352 is_rect: IsRect,
353 ) -> Option<Vec<SelectionFragment>> {
354 let mut selection_fragments = Vec::new();
355 for child in self.children.iter() {
356 if let Some(selectable_child) = child.element.as_selectable_element() {
357 if let Some(child_fragments) =
358 selectable_child.get_selection(selection_start, selection_end, is_rect)
359 {
360 selection_fragments.extend(child_fragments);
361 }
362 }
363 }
364 if !selection_fragments.is_empty() {
365 return Some(selection_fragments);
366 }
367 None
368 }
369 
370 fn expand_selection(
371 &self,
372 point: Vector2F,
373 direction: SelectionDirection,
374 unit: SelectionType,
375 word_boundaries_policy: &WordBoundariesPolicy,
376 ) -> Option<Vector2F> {
377 for child in self.children.iter() {
378 if let Some(selectable_child) = child.element.as_selectable_element() {
379 if let Some(selection) = selectable_child.expand_selection(
380 point,
381 direction,
382 unit,
383 word_boundaries_policy,
384 ) {
385 return Some(selection);
386 }
387 }
388 }
389 None
390 }
391 
392 fn is_point_semantically_before(
393 &self,
394 absolute_point: Vector2F,
395 absolute_point_other: Vector2F,
396 ) -> Option<bool> {
397 for child in self.children.iter() {
398 if let Some(selectable_child) = child.element.as_selectable_element() {
399 if let Some(is_point_semantically_before) = selectable_child
400 .is_point_semantically_before(absolute_point, absolute_point_other)
401 {
402 return Some(is_point_semantically_before);
403 }
404 }
405 }
406 None
407 }
408 
409 fn smart_select(
410 &self,
411 absolute_point: Vector2F,
412 smart_select_fn: crate::elements::SmartSelectFn,
413 ) -> Option<(Vector2F, Vector2F)> {
414 for child in self.children.iter() {
415 if let Some(selectable_child) = child.element.as_selectable_element() {
416 if let Some(selection) =
417 selectable_child.smart_select(absolute_point, smart_select_fn)
418 {
419 return Some(selection);
420 }
421 }
422 }
423 None
424 }
425 
426 fn calculate_clickable_bounds(&self, current_selection: Option<Selection>) -> Vec<RectF> {
427 let mut clickable_bounds = Vec::new();
428 for child in self.children.iter() {
429 if let Some(selectable_child) = child.element.as_selectable_element() {
430 clickable_bounds
431 .append(&mut selectable_child.calculate_clickable_bounds(current_selection));
432 }
433 }
434 clickable_bounds
435 }
436}
437 
438impl Extend<Box<dyn Element>> for Stack {
439 fn extend<T: IntoIterator<Item = Box<dyn Element>>>(&mut self, children: T) {
440 self.children
441 .extend(children.into_iter().map(StackChild::new))
442 }
443}
444 
445#[cfg(test)]
446#[path = "mod_test.rs"]
447mod tests;
448