flex.rs

  1use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
  2
  3use crate::{
  4    json::{self, ToJson, Value},
  5    AnyElement, Axis, Element, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder,
  6    SizeConstraint, Vector2FExt, ViewContext,
  7};
  8use pathfinder_geometry::{
  9    rect::RectF,
 10    vector::{vec2f, Vector2F},
 11};
 12use serde_json::json;
 13
 14#[derive(Default)]
 15struct ScrollState {
 16    scroll_to: Cell<Option<usize>>,
 17    scroll_position: Cell<f32>,
 18}
 19
 20pub struct Flex<V> {
 21    axis: Axis,
 22    children: Vec<AnyElement<V>>,
 23    scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
 24    child_alignment: f32,
 25    spacing: f32,
 26}
 27
 28impl<V: 'static> Flex<V> {
 29    pub fn new(axis: Axis) -> Self {
 30        Self {
 31            axis,
 32            children: Default::default(),
 33            scroll_state: None,
 34            child_alignment: -1.,
 35            spacing: 0.,
 36        }
 37    }
 38
 39    pub fn row() -> Self {
 40        Self::new(Axis::Horizontal)
 41    }
 42
 43    pub fn column() -> Self {
 44        Self::new(Axis::Vertical)
 45    }
 46
 47    /// Render children centered relative to the cross-axis of the parent flex.
 48    ///
 49    /// If this is a flex row, children will be centered vertically. If this is a
 50    /// flex column, children will be centered horizontally.
 51    pub fn align_children_center(mut self) -> Self {
 52        self.child_alignment = 0.;
 53        self
 54    }
 55
 56    pub fn with_spacing(mut self, spacing: f32) -> Self {
 57        self.spacing = spacing;
 58        self
 59    }
 60
 61    pub fn scrollable<Tag>(
 62        mut self,
 63        element_id: usize,
 64        scroll_to: Option<usize>,
 65        cx: &mut ViewContext<V>,
 66    ) -> Self
 67    where
 68        Tag: 'static,
 69    {
 70        let scroll_state = cx.default_element_state::<Tag, Rc<ScrollState>>(element_id);
 71        scroll_state.read(cx).scroll_to.set(scroll_to);
 72        self.scroll_state = Some((scroll_state, cx.handle().id()));
 73        self
 74    }
 75
 76    pub fn is_empty(&self) -> bool {
 77        self.children.is_empty()
 78    }
 79
 80    fn layout_flex_children(
 81        &mut self,
 82        layout_expanded: bool,
 83        constraint: SizeConstraint,
 84        remaining_space: &mut f32,
 85        remaining_flex: &mut f32,
 86        cross_axis_max: &mut f32,
 87        view: &mut V,
 88        cx: &mut LayoutContext<V>,
 89    ) {
 90        let cross_axis = self.axis.invert();
 91        for child in self.children.iter_mut() {
 92            if let Some(metadata) = child.metadata::<FlexParentData>() {
 93                if let Some((flex, expanded)) = metadata.flex {
 94                    if expanded != layout_expanded {
 95                        continue;
 96                    }
 97
 98                    let child_max = if *remaining_flex == 0.0 {
 99                        *remaining_space
100                    } else {
101                        let space_per_flex = *remaining_space / *remaining_flex;
102                        space_per_flex * flex
103                    };
104                    let child_min = if expanded { child_max } else { 0. };
105                    let child_constraint = match self.axis {
106                        Axis::Horizontal => SizeConstraint::new(
107                            vec2f(child_min, constraint.min.y()),
108                            vec2f(child_max, constraint.max.y()),
109                        ),
110                        Axis::Vertical => SizeConstraint::new(
111                            vec2f(constraint.min.x(), child_min),
112                            vec2f(constraint.max.x(), child_max),
113                        ),
114                    };
115                    let child_size = child.layout(child_constraint, view, cx);
116                    *remaining_space -= child_size.along(self.axis);
117                    *remaining_flex -= flex;
118                    *cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
119                }
120            }
121        }
122    }
123}
124
125impl<V> Extend<AnyElement<V>> for Flex<V> {
126    fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
127        self.children.extend(children);
128    }
129}
130
131impl<V: 'static> Element<V> for Flex<V> {
132    type LayoutState = f32;
133    type PaintState = ();
134
135    fn layout(
136        &mut self,
137        constraint: SizeConstraint,
138        view: &mut V,
139        cx: &mut LayoutContext<V>,
140    ) -> (Vector2F, Self::LayoutState) {
141        let mut total_flex = None;
142        let mut fixed_space = self.children.len().saturating_sub(1) as f32 * self.spacing;
143        let mut contains_float = false;
144
145        let cross_axis = self.axis.invert();
146        let mut cross_axis_max: f32 = 0.0;
147        for child in self.children.iter_mut() {
148            let metadata = child.metadata::<FlexParentData>();
149            contains_float |= metadata.map_or(false, |metadata| metadata.float);
150
151            if let Some(flex) = metadata.and_then(|metadata| metadata.flex.map(|(flex, _)| flex)) {
152                *total_flex.get_or_insert(0.) += flex;
153            } else {
154                let child_constraint = match self.axis {
155                    Axis::Horizontal => SizeConstraint::new(
156                        vec2f(0.0, constraint.min.y()),
157                        vec2f(INFINITY, constraint.max.y()),
158                    ),
159                    Axis::Vertical => SizeConstraint::new(
160                        vec2f(constraint.min.x(), 0.0),
161                        vec2f(constraint.max.x(), INFINITY),
162                    ),
163                };
164                let size = child.layout(child_constraint, view, cx);
165                fixed_space += size.along(self.axis);
166                cross_axis_max = cross_axis_max.max(size.along(cross_axis));
167            }
168        }
169
170        let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
171        let mut size = if let Some(mut remaining_flex) = total_flex {
172            if remaining_space.is_infinite() {
173                panic!("flex contains flexible children but has an infinite constraint along the flex axis");
174            }
175
176            self.layout_flex_children(
177                false,
178                constraint,
179                &mut remaining_space,
180                &mut remaining_flex,
181                &mut cross_axis_max,
182                view,
183                cx,
184            );
185            self.layout_flex_children(
186                true,
187                constraint,
188                &mut remaining_space,
189                &mut remaining_flex,
190                &mut cross_axis_max,
191                view,
192                cx,
193            );
194
195            match self.axis {
196                Axis::Horizontal => vec2f(constraint.max.x() - remaining_space, cross_axis_max),
197                Axis::Vertical => vec2f(cross_axis_max, constraint.max.y() - remaining_space),
198            }
199        } else {
200            match self.axis {
201                Axis::Horizontal => vec2f(fixed_space, cross_axis_max),
202                Axis::Vertical => vec2f(cross_axis_max, fixed_space),
203            }
204        };
205
206        if contains_float {
207            match self.axis {
208                Axis::Horizontal => size.set_x(size.x().max(constraint.max.x())),
209                Axis::Vertical => size.set_y(size.y().max(constraint.max.y())),
210            }
211        }
212
213        if constraint.min.x().is_finite() {
214            size.set_x(size.x().max(constraint.min.x()));
215        }
216        if constraint.min.y().is_finite() {
217            size.set_y(size.y().max(constraint.min.y()));
218        }
219
220        if size.x() > constraint.max.x() {
221            size.set_x(constraint.max.x());
222        }
223        if size.y() > constraint.max.y() {
224            size.set_y(constraint.max.y());
225        }
226
227        if let Some(scroll_state) = self.scroll_state.as_ref() {
228            scroll_state.0.update(cx.view_context(), |scroll_state, _| {
229                if let Some(scroll_to) = scroll_state.scroll_to.take() {
230                    let visible_start = scroll_state.scroll_position.get();
231                    let visible_end = visible_start + size.along(self.axis);
232                    if let Some(child) = self.children.get(scroll_to) {
233                        let child_start: f32 = self.children[..scroll_to]
234                            .iter()
235                            .map(|c| c.size().along(self.axis))
236                            .sum();
237                        let child_end = child_start + child.size().along(self.axis);
238                        if child_start < visible_start {
239                            scroll_state.scroll_position.set(child_start);
240                        } else if child_end > visible_end {
241                            scroll_state
242                                .scroll_position
243                                .set(child_end - size.along(self.axis));
244                        }
245                    }
246                }
247
248                scroll_state.scroll_position.set(
249                    scroll_state
250                        .scroll_position
251                        .get()
252                        .min(-remaining_space)
253                        .max(0.),
254                );
255            });
256        }
257
258        (size, remaining_space)
259    }
260
261    fn paint(
262        &mut self,
263        scene: &mut SceneBuilder,
264        bounds: RectF,
265        visible_bounds: RectF,
266        remaining_space: &mut Self::LayoutState,
267        view: &mut V,
268        cx: &mut PaintContext<V>,
269    ) -> Self::PaintState {
270        let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
271
272        let mut remaining_space = *remaining_space;
273        let overflowing = remaining_space < 0.;
274        if overflowing {
275            scene.push_layer(Some(visible_bounds));
276        }
277
278        if let Some(scroll_state) = &self.scroll_state {
279            scene.push_mouse_region(
280                crate::MouseRegion::new::<Self>(scroll_state.1, 0, bounds)
281                    .on_scroll({
282                        let scroll_state = scroll_state.0.read(cx).clone();
283                        let axis = self.axis;
284                        move |e, _: &mut V, cx| {
285                            if remaining_space < 0. {
286                                let scroll_delta = e.delta.raw();
287
288                                let mut delta = match axis {
289                                    Axis::Horizontal => {
290                                        if scroll_delta.x().abs() >= scroll_delta.y().abs() {
291                                            scroll_delta.x()
292                                        } else {
293                                            scroll_delta.y()
294                                        }
295                                    }
296                                    Axis::Vertical => scroll_delta.y(),
297                                };
298                                if !e.delta.precise() {
299                                    delta *= 20.;
300                                }
301
302                                scroll_state
303                                    .scroll_position
304                                    .set(scroll_state.scroll_position.get() - delta);
305
306                                cx.notify();
307                            } else {
308                                cx.propagate_event();
309                            }
310                        }
311                    })
312                    .on_move(|_, _: &mut V, _| { /* Capture move events */ }),
313            )
314        }
315
316        let mut child_origin = bounds.origin();
317        if let Some(scroll_state) = self.scroll_state.as_ref() {
318            let scroll_position = scroll_state.0.read(cx).scroll_position.get();
319            match self.axis {
320                Axis::Horizontal => child_origin.set_x(child_origin.x() - scroll_position),
321                Axis::Vertical => child_origin.set_y(child_origin.y() - scroll_position),
322            }
323        }
324
325        for child in self.children.iter_mut() {
326            if remaining_space > 0. {
327                if let Some(metadata) = child.metadata::<FlexParentData>() {
328                    if metadata.float {
329                        match self.axis {
330                            Axis::Horizontal => child_origin += vec2f(remaining_space, 0.0),
331                            Axis::Vertical => child_origin += vec2f(0.0, remaining_space),
332                        }
333                        remaining_space = 0.;
334                    }
335                }
336            }
337
338            // We use the child_alignment f32 to determine a point along the cross axis of the
339            // overall flex element and each child. We then align these points. So 0 would center
340            // each child relative to the overall height/width of the flex. -1 puts children at
341            // the start. 1 puts children at the end.
342            let aligned_child_origin = {
343                let cross_axis = self.axis.invert();
344                let my_center = bounds.size().along(cross_axis) / 2.;
345                let my_target = my_center + my_center * self.child_alignment;
346
347                let child_center = child.size().along(cross_axis) / 2.;
348                let child_target = child_center + child_center * self.child_alignment;
349
350                let mut aligned_child_origin = child_origin;
351                match self.axis {
352                    Axis::Horizontal => aligned_child_origin
353                        .set_y(aligned_child_origin.y() - (child_target - my_target)),
354                    Axis::Vertical => aligned_child_origin
355                        .set_x(aligned_child_origin.x() - (child_target - my_target)),
356                }
357
358                aligned_child_origin
359            };
360
361            child.paint(scene, aligned_child_origin, visible_bounds, view, cx);
362
363            match self.axis {
364                Axis::Horizontal => child_origin += vec2f(child.size().x() + self.spacing, 0.0),
365                Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + self.spacing),
366            }
367        }
368
369        if overflowing {
370            scene.pop_layer();
371        }
372    }
373
374    fn rect_for_text_range(
375        &self,
376        range_utf16: Range<usize>,
377        _: RectF,
378        _: RectF,
379        _: &Self::LayoutState,
380        _: &Self::PaintState,
381        view: &V,
382        cx: &ViewContext<V>,
383    ) -> Option<RectF> {
384        self.children
385            .iter()
386            .find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx))
387    }
388
389    fn debug(
390        &self,
391        bounds: RectF,
392        _: &Self::LayoutState,
393        _: &Self::PaintState,
394        view: &V,
395        cx: &ViewContext<V>,
396    ) -> json::Value {
397        json!({
398            "type": "Flex",
399            "bounds": bounds.to_json(),
400            "axis": self.axis.to_json(),
401            "children": self.children.iter().map(|child| child.debug(view, cx)).collect::<Vec<json::Value>>()
402        })
403    }
404}
405
406struct FlexParentData {
407    flex: Option<(f32, bool)>,
408    float: bool,
409}
410
411pub struct FlexItem<V> {
412    metadata: FlexParentData,
413    child: AnyElement<V>,
414}
415
416impl<V: 'static> FlexItem<V> {
417    pub fn new(child: impl Element<V>) -> Self {
418        FlexItem {
419            metadata: FlexParentData {
420                flex: None,
421                float: false,
422            },
423            child: child.into_any(),
424        }
425    }
426
427    pub fn flex(mut self, flex: f32, expanded: bool) -> Self {
428        self.metadata.flex = Some((flex, expanded));
429        self
430    }
431
432    pub fn float(mut self) -> Self {
433        self.metadata.float = true;
434        self
435    }
436}
437
438impl<V: 'static> Element<V> for FlexItem<V> {
439    type LayoutState = ();
440    type PaintState = ();
441
442    fn layout(
443        &mut self,
444        constraint: SizeConstraint,
445        view: &mut V,
446        cx: &mut LayoutContext<V>,
447    ) -> (Vector2F, Self::LayoutState) {
448        let size = self.child.layout(constraint, view, cx);
449        (size, ())
450    }
451
452    fn paint(
453        &mut self,
454        scene: &mut SceneBuilder,
455        bounds: RectF,
456        visible_bounds: RectF,
457        _: &mut Self::LayoutState,
458        view: &mut V,
459        cx: &mut PaintContext<V>,
460    ) -> Self::PaintState {
461        self.child
462            .paint(scene, bounds.origin(), visible_bounds, view, cx)
463    }
464
465    fn rect_for_text_range(
466        &self,
467        range_utf16: Range<usize>,
468        _: RectF,
469        _: RectF,
470        _: &Self::LayoutState,
471        _: &Self::PaintState,
472        view: &V,
473        cx: &ViewContext<V>,
474    ) -> Option<RectF> {
475        self.child.rect_for_text_range(range_utf16, view, cx)
476    }
477
478    fn metadata(&self) -> Option<&dyn Any> {
479        Some(&self.metadata)
480    }
481
482    fn debug(
483        &self,
484        _: RectF,
485        _: &Self::LayoutState,
486        _: &Self::PaintState,
487        view: &V,
488        cx: &ViewContext<V>,
489    ) -> Value {
490        json!({
491            "type": "Flexible",
492            "flex": self.metadata.flex,
493            "child": self.child.debug(view, cx)
494        })
495    }
496}