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, SizeConstraint,
  6    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        bounds: RectF,
264        visible_bounds: RectF,
265        remaining_space: &mut Self::LayoutState,
266        view: &mut V,
267        cx: &mut PaintContext<V>,
268    ) -> Self::PaintState {
269        let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
270
271        let mut remaining_space = *remaining_space;
272        let overflowing = remaining_space < 0.;
273        if overflowing {
274            cx.scene().push_layer(Some(visible_bounds));
275        }
276
277        if let Some((scroll_state, id)) = &self.scroll_state {
278            let scroll_state = scroll_state.read(cx).clone();
279            cx.scene().push_mouse_region(
280                crate::MouseRegion::new::<Self>(*id, 0, bounds)
281                    .on_scroll({
282                        let axis = self.axis;
283                        move |e, _: &mut V, cx| {
284                            if remaining_space < 0. {
285                                let scroll_delta = e.delta.raw();
286
287                                let mut delta = match axis {
288                                    Axis::Horizontal => {
289                                        if scroll_delta.x().abs() >= scroll_delta.y().abs() {
290                                            scroll_delta.x()
291                                        } else {
292                                            scroll_delta.y()
293                                        }
294                                    }
295                                    Axis::Vertical => scroll_delta.y(),
296                                };
297                                if !e.delta.precise() {
298                                    delta *= 20.;
299                                }
300
301                                scroll_state
302                                    .scroll_position
303                                    .set(scroll_state.scroll_position.get() - delta);
304
305                                cx.notify();
306                            } else {
307                                cx.propagate_event();
308                            }
309                        }
310                    })
311                    .on_move(|_, _: &mut V, _| { /* Capture move events */ }),
312            )
313        }
314
315        let mut child_origin = bounds.origin();
316        if let Some(scroll_state) = self.scroll_state.as_ref() {
317            let scroll_position = scroll_state.0.read(cx).scroll_position.get();
318            match self.axis {
319                Axis::Horizontal => child_origin.set_x(child_origin.x() - scroll_position),
320                Axis::Vertical => child_origin.set_y(child_origin.y() - scroll_position),
321            }
322        }
323
324        for child in self.children.iter_mut() {
325            if remaining_space > 0. {
326                if let Some(metadata) = child.metadata::<FlexParentData>() {
327                    if metadata.float {
328                        match self.axis {
329                            Axis::Horizontal => child_origin += vec2f(remaining_space, 0.0),
330                            Axis::Vertical => child_origin += vec2f(0.0, remaining_space),
331                        }
332                        remaining_space = 0.;
333                    }
334                }
335            }
336
337            // We use the child_alignment f32 to determine a point along the cross axis of the
338            // overall flex element and each child. We then align these points. So 0 would center
339            // each child relative to the overall height/width of the flex. -1 puts children at
340            // the start. 1 puts children at the end.
341            let aligned_child_origin = {
342                let cross_axis = self.axis.invert();
343                let my_center = bounds.size().along(cross_axis) / 2.;
344                let my_target = my_center + my_center * self.child_alignment;
345
346                let child_center = child.size().along(cross_axis) / 2.;
347                let child_target = child_center + child_center * self.child_alignment;
348
349                let mut aligned_child_origin = child_origin;
350                match self.axis {
351                    Axis::Horizontal => aligned_child_origin
352                        .set_y(aligned_child_origin.y() - (child_target - my_target)),
353                    Axis::Vertical => aligned_child_origin
354                        .set_x(aligned_child_origin.x() - (child_target - my_target)),
355                }
356
357                aligned_child_origin
358            };
359
360            child.paint(aligned_child_origin, visible_bounds, view, cx);
361
362            match self.axis {
363                Axis::Horizontal => child_origin += vec2f(child.size().x() + self.spacing, 0.0),
364                Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + self.spacing),
365            }
366        }
367
368        if overflowing {
369            cx.scene().pop_layer();
370        }
371    }
372
373    fn rect_for_text_range(
374        &self,
375        range_utf16: Range<usize>,
376        _: RectF,
377        _: RectF,
378        _: &Self::LayoutState,
379        _: &Self::PaintState,
380        view: &V,
381        cx: &ViewContext<V>,
382    ) -> Option<RectF> {
383        self.children
384            .iter()
385            .find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx))
386    }
387
388    fn debug(
389        &self,
390        bounds: RectF,
391        _: &Self::LayoutState,
392        _: &Self::PaintState,
393        view: &V,
394        cx: &ViewContext<V>,
395    ) -> json::Value {
396        json!({
397            "type": "Flex",
398            "bounds": bounds.to_json(),
399            "axis": self.axis.to_json(),
400            "children": self.children.iter().map(|child| child.debug(view, cx)).collect::<Vec<json::Value>>()
401        })
402    }
403}
404
405struct FlexParentData {
406    flex: Option<(f32, bool)>,
407    float: bool,
408}
409
410pub struct FlexItem<V> {
411    metadata: FlexParentData,
412    child: AnyElement<V>,
413}
414
415impl<V: 'static> FlexItem<V> {
416    pub fn new(child: impl Element<V>) -> Self {
417        FlexItem {
418            metadata: FlexParentData {
419                flex: None,
420                float: false,
421            },
422            child: child.into_any(),
423        }
424    }
425
426    pub fn flex(mut self, flex: f32, expanded: bool) -> Self {
427        self.metadata.flex = Some((flex, expanded));
428        self
429    }
430
431    pub fn float(mut self) -> Self {
432        self.metadata.float = true;
433        self
434    }
435}
436
437impl<V: 'static> Element<V> for FlexItem<V> {
438    type LayoutState = ();
439    type PaintState = ();
440
441    fn layout(
442        &mut self,
443        constraint: SizeConstraint,
444        view: &mut V,
445        cx: &mut LayoutContext<V>,
446    ) -> (Vector2F, Self::LayoutState) {
447        let size = self.child.layout(constraint, view, cx);
448        (size, ())
449    }
450
451    fn paint(
452        &mut self,
453        bounds: RectF,
454        visible_bounds: RectF,
455        _: &mut Self::LayoutState,
456        view: &mut V,
457        cx: &mut PaintContext<V>,
458    ) -> Self::PaintState {
459        self.child.paint(bounds.origin(), visible_bounds, view, cx)
460    }
461
462    fn rect_for_text_range(
463        &self,
464        range_utf16: Range<usize>,
465        _: RectF,
466        _: RectF,
467        _: &Self::LayoutState,
468        _: &Self::PaintState,
469        view: &V,
470        cx: &ViewContext<V>,
471    ) -> Option<RectF> {
472        self.child.rect_for_text_range(range_utf16, view, cx)
473    }
474
475    fn metadata(&self) -> Option<&dyn Any> {
476        Some(&self.metadata)
477    }
478
479    fn debug(
480        &self,
481        _: RectF,
482        _: &Self::LayoutState,
483        _: &Self::PaintState,
484        view: &V,
485        cx: &ViewContext<V>,
486    ) -> Value {
487        json!({
488            "type": "Flexible",
489            "flex": self.metadata.flex,
490            "child": self.child.debug(view, cx)
491        })
492    }
493}