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