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        let last = self.children.len() - 1;
 92        for (ix, child) in &mut self.children.iter_mut().enumerate() {
 93            if let Some(metadata) = child.metadata::<FlexParentData>() {
 94                if let Some((flex, expanded)) = metadata.flex {
 95                    if expanded != layout_expanded {
 96                        continue;
 97                    }
 98
 99                    let child_max = if *remaining_flex == 0.0 {
100                        *remaining_space
101                    } else {
102                        let space_per_flex = *remaining_space / *remaining_flex;
103                        space_per_flex * flex
104                    } - if ix == 0 || ix == last {
105                        self.spacing / 2.
106                    } else {
107                        self.spacing
108                    };
109                    let child_min = if expanded { child_max } else { 0. };
110                    let child_constraint = match self.axis {
111                        Axis::Horizontal => SizeConstraint::new(
112                            vec2f(child_min, constraint.min.y()),
113                            vec2f(child_max, constraint.max.y()),
114                        ),
115                        Axis::Vertical => SizeConstraint::new(
116                            vec2f(constraint.min.x(), child_min),
117                            vec2f(constraint.max.x(), child_max),
118                        ),
119                    };
120                    let child_size = child.layout(child_constraint, view, cx);
121                    *remaining_space -= child_size.along(self.axis);
122                    *remaining_flex -= flex;
123                    *cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
124                }
125            }
126        }
127    }
128}
129
130impl<V> Extend<AnyElement<V>> for Flex<V> {
131    fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
132        self.children.extend(children);
133    }
134}
135
136impl<V: 'static> Element<V> for Flex<V> {
137    type LayoutState = f32;
138    type PaintState = ();
139
140    fn layout(
141        &mut self,
142        constraint: SizeConstraint,
143        view: &mut V,
144        cx: &mut LayoutContext<V>,
145    ) -> (Vector2F, Self::LayoutState) {
146        let mut total_flex = None;
147        let mut fixed_space = 0.0;
148        let mut contains_float = false;
149
150        let cross_axis = self.axis.invert();
151        let mut cross_axis_max: f32 = 0.0;
152        let last = self.children.len().saturating_sub(1);
153        for (ix, child) in &mut self.children.iter_mut().enumerate() {
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                    + if ix == 0 || ix == last {
173                        self.spacing / 2.
174                    } else {
175                        self.spacing
176                    };
177                cross_axis_max = cross_axis_max.max(size.along(cross_axis));
178            }
179        }
180
181        let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
182        let mut size = if let Some(mut remaining_flex) = total_flex {
183            if remaining_space.is_infinite() {
184                panic!("flex contains flexible children but has an infinite constraint along the flex axis");
185            }
186
187            self.layout_flex_children(
188                false,
189                constraint,
190                &mut remaining_space,
191                &mut remaining_flex,
192                &mut cross_axis_max,
193                view,
194                cx,
195            );
196            self.layout_flex_children(
197                true,
198                constraint,
199                &mut remaining_space,
200                &mut remaining_flex,
201                &mut cross_axis_max,
202                view,
203                cx,
204            );
205
206            match self.axis {
207                Axis::Horizontal => vec2f(constraint.max.x() - remaining_space, cross_axis_max),
208                Axis::Vertical => vec2f(cross_axis_max, constraint.max.y() - remaining_space),
209            }
210        } else {
211            match self.axis {
212                Axis::Horizontal => vec2f(fixed_space, cross_axis_max),
213                Axis::Vertical => vec2f(cross_axis_max, fixed_space),
214            }
215        };
216
217        if contains_float {
218            match self.axis {
219                Axis::Horizontal => size.set_x(size.x().max(constraint.max.x())),
220                Axis::Vertical => size.set_y(size.y().max(constraint.max.y())),
221            }
222        }
223
224        if constraint.min.x().is_finite() {
225            size.set_x(size.x().max(constraint.min.x()));
226        }
227        if constraint.min.y().is_finite() {
228            size.set_y(size.y().max(constraint.min.y()));
229        }
230
231        if size.x() > constraint.max.x() {
232            size.set_x(constraint.max.x());
233        }
234        if size.y() > constraint.max.y() {
235            size.set_y(constraint.max.y());
236        }
237
238        if let Some(scroll_state) = self.scroll_state.as_ref() {
239            scroll_state.0.update(cx.view_context(), |scroll_state, _| {
240                if let Some(scroll_to) = scroll_state.scroll_to.take() {
241                    let visible_start = scroll_state.scroll_position.get();
242                    let visible_end = visible_start + size.along(self.axis);
243                    if let Some(child) = self.children.get(scroll_to) {
244                        let child_start: f32 = self.children[..scroll_to]
245                            .iter()
246                            .map(|c| c.size().along(self.axis))
247                            .sum();
248                        let child_end = child_start + child.size().along(self.axis);
249                        if child_start < visible_start {
250                            scroll_state.scroll_position.set(child_start);
251                        } else if child_end > visible_end {
252                            scroll_state
253                                .scroll_position
254                                .set(child_end - size.along(self.axis));
255                        }
256                    }
257                }
258
259                scroll_state.scroll_position.set(
260                    scroll_state
261                        .scroll_position
262                        .get()
263                        .min(-remaining_space)
264                        .max(0.),
265                );
266            });
267        }
268
269        (size, remaining_space)
270    }
271
272    fn paint(
273        &mut self,
274        scene: &mut SceneBuilder,
275        bounds: RectF,
276        visible_bounds: RectF,
277        remaining_space: &mut Self::LayoutState,
278        view: &mut V,
279        cx: &mut PaintContext<V>,
280    ) -> Self::PaintState {
281        let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
282
283        let mut remaining_space = *remaining_space;
284        let overflowing = remaining_space < 0.;
285        if overflowing {
286            scene.push_layer(Some(visible_bounds));
287        }
288
289        if let Some(scroll_state) = &self.scroll_state {
290            scene.push_mouse_region(
291                crate::MouseRegion::new::<Self>(scroll_state.1, 0, bounds)
292                    .on_scroll({
293                        let scroll_state = scroll_state.0.read(cx).clone();
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        let last = self.children.len().saturating_sub(1);
337        for (ix, child) in &mut self.children.iter_mut().enumerate() {
338            if remaining_space > 0. {
339                if let Some(metadata) = child.metadata::<FlexParentData>() {
340                    if metadata.float {
341                        match self.axis {
342                            Axis::Horizontal => child_origin += vec2f(remaining_space, 0.0),
343                            Axis::Vertical => child_origin += vec2f(0.0, remaining_space),
344                        }
345                        remaining_space = 0.;
346                    }
347                }
348            }
349
350            // We use the child_alignment f32 to determine a point along the cross axis of the
351            // overall flex element and each child. We then align these points. So 0 would center
352            // each child relative to the overall height/width of the flex. -1 puts children at
353            // the start. 1 puts children at the end.
354            let aligned_child_origin = {
355                let cross_axis = self.axis.invert();
356                let my_center = bounds.size().along(cross_axis) / 2.;
357                let my_target = my_center + my_center * self.child_alignment;
358
359                let child_center = child.size().along(cross_axis) / 2.;
360                let child_target = child_center + child_center * self.child_alignment;
361
362                let mut aligned_child_origin = child_origin;
363                match self.axis {
364                    Axis::Horizontal => aligned_child_origin
365                        .set_y(aligned_child_origin.y() - (child_target - my_target)),
366                    Axis::Vertical => aligned_child_origin
367                        .set_x(aligned_child_origin.x() - (child_target - my_target)),
368                }
369
370                aligned_child_origin
371            };
372
373            child.paint(scene, aligned_child_origin, visible_bounds, view, cx);
374
375            let spacing = if ix == last { 0. } else { self.spacing };
376
377            match self.axis {
378                Axis::Horizontal => child_origin += vec2f(child.size().x() + spacing, 0.0),
379                Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + spacing),
380            }
381        }
382
383        if overflowing {
384            scene.pop_layer();
385        }
386    }
387
388    fn rect_for_text_range(
389        &self,
390        range_utf16: Range<usize>,
391        _: RectF,
392        _: RectF,
393        _: &Self::LayoutState,
394        _: &Self::PaintState,
395        view: &V,
396        cx: &ViewContext<V>,
397    ) -> Option<RectF> {
398        self.children
399            .iter()
400            .find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx))
401    }
402
403    fn debug(
404        &self,
405        bounds: RectF,
406        _: &Self::LayoutState,
407        _: &Self::PaintState,
408        view: &V,
409        cx: &ViewContext<V>,
410    ) -> json::Value {
411        json!({
412            "type": "Flex",
413            "bounds": bounds.to_json(),
414            "axis": self.axis.to_json(),
415            "children": self.children.iter().map(|child| child.debug(view, cx)).collect::<Vec<json::Value>>()
416        })
417    }
418}
419
420struct FlexParentData {
421    flex: Option<(f32, bool)>,
422    float: bool,
423}
424
425pub struct FlexItem<V> {
426    metadata: FlexParentData,
427    child: AnyElement<V>,
428}
429
430impl<V: 'static> FlexItem<V> {
431    pub fn new(child: impl Element<V>) -> Self {
432        FlexItem {
433            metadata: FlexParentData {
434                flex: None,
435                float: false,
436            },
437            child: child.into_any(),
438        }
439    }
440
441    pub fn flex(mut self, flex: f32, expanded: bool) -> Self {
442        self.metadata.flex = Some((flex, expanded));
443        self
444    }
445
446    pub fn float(mut self) -> Self {
447        self.metadata.float = true;
448        self
449    }
450}
451
452impl<V: 'static> Element<V> for FlexItem<V> {
453    type LayoutState = ();
454    type PaintState = ();
455
456    fn layout(
457        &mut self,
458        constraint: SizeConstraint,
459        view: &mut V,
460        cx: &mut LayoutContext<V>,
461    ) -> (Vector2F, Self::LayoutState) {
462        let size = self.child.layout(constraint, view, cx);
463        (size, ())
464    }
465
466    fn paint(
467        &mut self,
468        scene: &mut SceneBuilder,
469        bounds: RectF,
470        visible_bounds: RectF,
471        _: &mut Self::LayoutState,
472        view: &mut V,
473        cx: &mut PaintContext<V>,
474    ) -> Self::PaintState {
475        self.child
476            .paint(scene, bounds.origin(), visible_bounds, view, cx)
477    }
478
479    fn rect_for_text_range(
480        &self,
481        range_utf16: Range<usize>,
482        _: RectF,
483        _: RectF,
484        _: &Self::LayoutState,
485        _: &Self::PaintState,
486        view: &V,
487        cx: &ViewContext<V>,
488    ) -> Option<RectF> {
489        self.child.rect_for_text_range(range_utf16, view, cx)
490    }
491
492    fn metadata(&self) -> Option<&dyn Any> {
493        Some(&self.metadata)
494    }
495
496    fn debug(
497        &self,
498        _: RectF,
499        _: &Self::LayoutState,
500        _: &Self::PaintState,
501        view: &V,
502        cx: &ViewContext<V>,
503    ) -> Value {
504        json!({
505            "type": "Flexible",
506            "flex": self.metadata.flex,
507            "child": self.child.debug(view, cx)
508        })
509    }
510}