flex.rs

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